commit 19708f9225db83e631e80306b54fb60236d4b2da parent 12ad8fe7c5fb90db84e26f3affbd148fb55da847 Author: dwrz <dwrz@dwrz.net> Date: Fri, 18 Sep 2026 18:20:29 +0000 Remove web cmd Replaced with org-mode generated site. Diffstat:
74 files changed, 0 insertions(+), 5773 deletions(-)
diff --git a/README.org b/README.org @@ -34,9 +34,6 @@ Send email via Gmail from the command line. * statusbar System status bar used with i3wm. -* web -Personal website. - * wen #+html: <p align="center"><img src="https://dwrz.net/static/media/wen.jpg" /></p> diff --git a/cmd/web/config/config.go b/cmd/web/config/config.go @@ -1,26 +0,0 @@ -package config - -import ( - _ "embed" - "encoding/json" - "fmt" - - "code.dwrz.net/src/pkg/server" -) - -//go:embed config.json -var configuration []byte - -type Config struct { - Debug bool `json:"debug"` - Server *server.Config `json:"server"` -} - -func New() (*Config, error) { - var cfg = &Config{} - if err := json.Unmarshal(configuration, cfg); err != nil { - return nil, fmt.Errorf("failed to parse config: %v", err) - } - - return cfg, nil -} diff --git a/cmd/web/config/config.template.json b/cmd/web/config/config.template.json @@ -1,17 +0,0 @@ -{ - "debug": true, - "server": { - "certPath": "", - "keyPath": "", - "maxHeaderBytes": 1024, - "ports": { - "http": "8080" - }, - "timeouts": { - "idle": 24000000000, - "read": 8000000000, - "shutdown": 30000000000, - "write": 16000000000 - } - } -} diff --git a/cmd/web/main.go b/cmd/web/main.go @@ -1,61 +0,0 @@ -package main - -import ( - "os" - "os/signal" - "syscall" - - "code.dwrz.net/src/cmd/web/config" - "code.dwrz.net/src/cmd/web/site" - "code.dwrz.net/src/pkg/log" - "code.dwrz.net/src/pkg/server" -) - -func main() { - var l = log.New(os.Stderr) - - // Get the config. - cfg, err := config.New() - if err != nil { - l.Error.Fatalf("failed to get config: %v", err) - } - - // Setup the site. - site, err := site.New(site.Params{ - Debug: cfg.Debug, - Log: l, - }) - if err != nil { - l.Error.Fatalf("failed to setup API: %v", err) - } - - // Setup the HTTP(S) server(s). - srv, err := server.New(server.Parameters{ - Config: *cfg.Server, - Handler: site, - Log: l, - }) - if err != nil { - l.Error.Fatalf("failed to create server: %v", err) - } - - // Serve the site. - srv.Serve() - - // Listen for OS signals. - osListener := make(chan os.Signal, 1) - signal.Notify( - osListener, - syscall.SIGTERM, syscall.SIGINT, - ) - - // Block until we receive a signal. - s := <-osListener - l.Debug.Printf("received signal: %s", s) - - if err := srv.Shutdown(); err != nil { - l.Error.Fatalf("failed to shutdown server: %v", err) - } - - l.Debug.Printf("terminating") -} diff --git a/cmd/web/site/entry/entry.go b/cmd/web/site/entry/entry.go @@ -1,149 +0,0 @@ -package entry - -import ( - "embed" - "encoding/json" - "fmt" - "html/template" - "io/fs" - "path/filepath" - "sort" - "time" - - "code.dwrz.net/src/pkg/log" -) - -//go:embed static/* -var static embed.FS - -const ( - YearFormat = "2006" - DateFormat = "2006-01-02" - metadataFile = "metadata.json" -) - -type Entry struct { - Cover string `json:"cover"` - Content template.HTML `json:"content"` - Date time.Time `json:"date"` - Link string `json:"link"` - Next *Entry `json:"next"` - Previous *Entry `json:"previous"` - Published bool `json:"published"` - Title string `json:"title"` -} - -type Year struct { - Entries []*Entry - Text string -} - -type LoadParams struct { - Log *log.Logger -} - -func Load(p LoadParams) ([]*Entry, error) { - var entries []*Entry - - parseFS := func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if !d.IsDir() || d.Name() == "static" { - return nil - } - - // Open the entry metadata file. - data, err := static.ReadFile( - filepath.Join("static", d.Name(), metadataFile), - ) - if err != nil { - p.Log.Error.Printf("ignoring %s: %v", d.Name(), err) - return nil - } - - var entry = &Entry{} - if err := json.Unmarshal(data, entry); err != nil { - p.Log.Error.Printf("ignoring %s: %v", d.Name(), err) - return nil - } - - // Ignore unpublished entries. - if !entry.Published { - p.Log.Debug.Printf( - "ignoring %s: not published", d.Name(), - ) - return nil - } - - // Get the entry content. - entryFile := d.Name() + ".html" - content, err := static.ReadFile( - filepath.Join("static", d.Name(), entryFile), - ) - if err != nil { - p.Log.Error.Printf("ignoring %s: %v", d.Name(), err) - return nil - } - - // Set entry values. - entry.Content = template.HTML(string(content)) - entry.Link = fmt.Sprintf("%s", entry.Date.Format(DateFormat)) - - entries = append(entries, entry) - - return nil - } - - if err := fs.WalkDir(static, "static", parseFS); err != nil { - return nil, fmt.Errorf("failed to parse templates: %v", err) - } - - // Sort the entries. - sort.Slice(entries, func(i, j int) bool { - return entries[i].Date.Before(entries[j].Date) - }) - - // Set the previous and next entry. - for i, e := range entries { - if i-1 >= 0 { - e.Previous = entries[i-1] - } - if i+1 < len(entries) { - e.Next = entries[i+1] - } - } - - return entries, nil -} - -func SortYear(entries []*Entry) []Year { - var yearEntries = map[string]*Year{} - for _, e := range entries { - year := e.Date.Format(YearFormat) - if _, exists := yearEntries[year]; !exists { - yearEntries[year] = &Year{ - Entries: []*Entry{e}, - Text: year, - } - continue - } - - yearEntries[year].Entries = append(yearEntries[year].Entries, e) - } - - // Sort each year's entries, then sort the years. - var years = []Year{} - for _, year := range yearEntries { - sort.Slice(year.Entries, func(i, j int) bool { - return year.Entries[i].Date.After(year.Entries[j].Date) - }) - - years = append(years, *year) - } - sort.Slice(years, func(i, j int) bool { - return years[i].Text > years[j].Text - }) - - return years -} diff --git a/cmd/web/site/entry/static/1987-09-01/1987-09-01.html b/cmd/web/site/entry/static/1987-09-01/1987-09-01.html @@ -1,26 +0,0 @@ -<div class="wide64"> - <a href="/static/media/1920/1987-09_1.jpg"> - <img class="img-center" src="/static/media/720/1987-09_1.jpg"> - </a> - <a href="/static/media/1920/1987-09_2.jpg"> - <img class="img-center" src="/static/media/720/1987-09_2.jpg"> - </a> - <a href="/static/media/1920/1987-09_3.jpg"> - <img class="img-center" src="/static/media/720/1987-09_3.jpg"> - </a> - <a href="/static/media/1920/1987-09_4.jpg"> - <img class="img-center" src="/static/media/720/1987-09_4.jpg"> - </a> - <a href="/static/media/1920/1987-09_5.jpg"> - <img class="img-center" src="/static/media/720/1987-09_5.jpg"> - </a> - <a href="/static/media/1920/1987-09_6.jpg"> - <img class="img-center" src="/static/media/720/1987-09_6.jpg"> - </a> - <a href="/static/media/1920/1987-09_7.jpg"> - <img class="img-center" src="/static/media/720/1987-09_7.jpg"> - </a> - <a href="/static/media/1920/1987-09_8.jpg"> - <img class="img-center" src="/static/media/720/1987-09_8.jpg"> - </a> -</div> diff --git a/cmd/web/site/entry/static/1987-09-01/metadata.json b/cmd/web/site/entry/static/1987-09-01/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "1987-09_1.jpg", - "date": "1987-09-01T00:00:00Z", - "published": true, - "title": "First Photographs" -} diff --git a/cmd/web/site/entry/static/2019-02-16/2019-02-16.html b/cmd/web/site/entry/static/2019-02-16/2019-02-16.html @@ -1,417 +0,0 @@ -<div class="wide64"> - <p> - I dream of seeing the end of the fossil fuel age in my lifetime. I would - love to play a role, however small, in helping to bring the era to a close. - And to help the next one — the ecological age — off the ground. - <br> - </p> - <p> - But I acknowledge that in that mission at best there's irony, at worst, - hypocrisy. It's unlikely someone with my background would exist in a world - without petroleum. <a href="https://www.thomhartmann.com/blog/2007/11/last-hours-ancient-sunlight">Ancient sunlight</a> played the matchmaker in my parents' marriage. - </p> - <p> - Unlike many immigrants, I've been lucky enough to return home throughout my - life. I treasure the connection I've been able to maintain with my family. - But I also realize it comes at a cost. - </p> - <p> - It takes 150 trees a year to sequester the carbon from a flight from Newark - to Shanghai. It takes a tree about 40 years to sequester a ton of carbon. A - flight from NYC to London is about 1 ton. I wonder how much arctic ice has - melted, how many <a href="https://www.theguardian.com/world/2019/feb/11/russian-islands-emergency-mass-invasion-polar-bears-novaya-zemlya">polar bear cubs have had to starve</a>, - so that I could live this unnatural life, crisscrossing the globe like one - my ancestors' deities. - </p> - <p> - The ethical choice would be not to travel. At the very least to not travel - so often. That would be better for the planet and most its inhabitants. The - cost in that case would be personal, limited at most to my family. But so - far, I have failed to muster the courage. - </p> - <p> - In a sense, my travels "home" are days spent listening to sirens' song. The - return ticket is the lash that binds me to the mast. The truth is that I am - never home. Wherever I am, family, rhythms, the earliest memories, are - somewhere else. - </p> - <p> - And yet, there's the irresistible embrace of the song. The feeling of - safety and belonging. The joy of returning to land, natural terrain; for - even after all these years, New York City still feels a little like being - at sea. A familiar ship at best, a fortress in the archipelago of Cyclopses - and lotus eaters. But not home. Not Ithaca. - </p> - <a href="/static/media/1920/dwrz_20190205T195842.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190205T195842.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190205T174305.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190205T174305.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190206T070449.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190206T070449.jpg"> - </a> - <p> - My last stay in Shanghai was in January and February of 2017, also for the - Lunar New Year. Before that trip, I was absent for five years – - probably the longest span in my life so far. In 2017, I felt like I was - reviving long lost memories; this year felt more like return to a natural - rhythm. - </p> - <p> - Winter still feels like an unusual time to be in China. Most of my memories - of China are of summer. Besides 2017, my memories of China in winter are - mainly from 2006-2007 (December and January), then maybe one or two visits - as a child. One of my earliest childhood memories of China is setting of - fireworks with my uncle Ju Gong (朱巨公), outside the old home on Li Shan Lu. - </p> - <a href="/static/media/1920/dwrz_20190206T102651.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190206T102651.jpg"> - </a> - <blockquote> - <p> - <i>Waiting for the tea lying pillowed in the breeze,</i> - <i>Spring is in the voice now that the heart's at ease.</i> - </p> - - <p> - Journey to the West, Chapter 64. - </p> - </blockquote> - <a href="/static/media/1920/dwrz_20190206T162939.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190206T162939.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190206T115448.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190206T115448.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190207T175201.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190207T175201.jpg"> - </a> - <p> - I spent the majority of this stay within a 500 meter radius of the new - apartment on Yan Chang Lu. Besides catching up with family, I spent some - time on personal projects: getting this website up and running again (using - <a href="https://orgmode.org/">org-mode</a>), learning <a href="https://en.wikipedia.org/wiki/Emacs_Lisp">Emacs Lisp</a> and <a href="https://en.wikipedia.org/wiki/X86_assembly_language">x86 Assembly</a>, and catching up on some more practical reading (<a href="https://en.wikipedia.org/wiki/The_Millionaire_Next_Door">The Millionaire Next Door</a>, <a href="https://en.wikipedia.org/w/index.php?title=The_Life_Changing_Magic_of_Tidying">The Life Changing Magic of Tidying Up</a>). Once or twice a day I would - take a walk in <a href="https://en.wikipedia.org/wiki/Zhabei">ZhaBei</a> - Park (闸北公园), to enjoy the scenery and think a little bit about life. - </p> - <a href="/static/media/1920/dwrz_20190207T180027.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190207T180027.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190207T170132.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190207T170132.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190207T170504.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190207T170504.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190207T172035.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190207T172035.jpg"> - </a> - <p> - Shanghai shuts down the week of the Lunar New Year. Nearly all of the local shops and restaurants were closed, and the streets were relatively empty, until about the last two days of my stay. - </p> - <p> - Unlike 2017, I didn't bring my bicycle. For the most part I didn't miss it, as this was a more sedentary stay. In the long term, I will try to keep one in Shanghai. It's a great way to get around the city. - </p> - <a href="/static/media/1920/dwrz_20190208T052925.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190208T052925.jpg"> - </a> - <p> - My alarm clock every morning was birdsong. A pleasant surprise, given the heavily urban setting. - </p> - <a href="/static/media/1920/dwrz_20190208T071951.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190208T071951.jpg"> - </a> - <p> - This was my first time seeing snow in Shanghai. ZhaBei Park is visible from the apartment, and vice-versa. - </p> - <a href="/static/media/1920/dwrz_20190209T140913.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190209T140913.jpg"> - </a> - <video autoplay loop muted - src="/static/media/dwrz_20190209T142546_720p.mp4" type="video/mp4"> - Your browser does not support video. - </video> - <p> - Most of my family doesn't know how to play Mahjong, as it was banned, and stigmatized, during their youth. - </p> - <video controls> - <source src="/static/media/dwrz_20190209T140220_edit.mp4" - type="video/mp4"> - Your browser does not support video. - </video> - <a href="/static/media/1920/dwrz_20190210T154420.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190210T154420.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190210T154429.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190210T154429.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190210T172452.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190210T172452.jpg"> - </a> - <p> - Authentic Chinese cuisine paints with a different palette. There are nuances of flavor that are hard to find abroad. - </p> - <a href="/static/media/1920/dwrz-20190210T124458.jpg"> - <img class ="img-center" src="/static/media/720/dwrz-20190210T124458.jpg"> - </a> - <p> - My grandfather and grandmother, with my cousin Zhu Yun. - </p> - <a href="/static/media/1920/dwrz_20190210T125410.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190210T125410.jpg"> - </a> - <p> - My brother and I with my grandfather. August of 1999. - </p> - <a href="/static/media/1920/dwrz_20190210T125837.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190210T125837.jpg"> - </a> - <p> - With my grandmother and cousin Zhu Wei Yi (Kim). August of 1997. - </p> - <a href="/static/media/1920/dwrz_20190210T123606.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190210T123606.jpg"> - </a> - <p> - With my parents. August of 2003. - </p> - <a href="/static/media/1920/dwrz_20190210T220450.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190210T220450.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190211T111406.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190211T111406.jpg"> - </a> - <p> - Free dance class in ZhaBei park. These seemed to be offered multiple times - a day, and were very popular with the elderly population. - </p> - <p> - I was struck by how much of the community seemed to congregate in the park. - Besides these classes, people practiced Tai Chi together, played Chinese - chess and musical instruments, and sang in choruses. Others, like me, - seemed to be happy to roam in the park, chat with friends, or meditate - among the scenery. - </p> - <p> - I struggle to think of a comparable community life in New York City. - Someday, I hope I will live among elders that emanate the same health, - contentment, and ease I saw in ZhaBei. - </p> - <a href="/static/media/1920/dwrz_20190211T111537.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190211T111537.jpg"> - </a> - <video controls> - <source src="/static/media/dwrz_20190211T160813_edit.mp4" type="video/mp4"> - Your browser does not support video. - </video> - <a href="/static/media/1920/dwrz_20190211T112317.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190211T112317.jpg"> - </a> - <p> - Sign placed above urinals in Zhabei Park's mens restroom. There is a lot to - the Chinese regime, and its history, that is dark. The lingering spirit of - collaboration and social camaraderie still manages to shine through. - </p> - <a href="/static/media/1920/dwrz_20190211T112925.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190211T112925.jpg"> - </a> - <p> - This machine, widely used, another example. - </p> - <a href="/static/media/1920/dwrz_20190211T112939.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190211T112939.jpg"> - </a> - <p> - Perhaps, if we had more PM 2.5 displays, worldwide, more people would be - able to quantify the value of environmental contexts. - </p> - <a href="/static/media/1920/dwrz_20190211T113930.jpg"> - <img class ="img-center" src="/static/media/720/dwrz_20190211T113930.jpg" - </a> - <a href="/static/media/1920/dwrz_20190211T153903.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190211T153903.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190211T162500.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190211T162500.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190212T104711.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190212T104711.jpg"> - </a> - <p> - At the local market, vendors are now using Alipay. You scan the QR code to - place your payment. Two years ago, it was still cash. Perhaps more slowly - now, but still the signs are of a nation ascendant. - </p> - <p> - Also heartening was the strong presence of small businesses in my - neighborhood. This made me note the difference in the application of - technology. Supporting small businesses instead of a monolithic retailer. - The flourishing of the just as convenient as the latter, without the - resulting social ills. - </p> - <a href="/static/media/1920/ylj_20190212T180734.jpg"> - <img class="img-center" src="/static/media/720/ylj_20190212T180734.jpg"> - </a> - <p> - With my uncle, Zhu Ju Qi, and my nephew – my cousin Zhu Yun's son. - </p> - <a href="/static/media/1920/ylj_20190212T180644.jpg"> - <img class="img-center" src="/static/media/720/ylj_20190212T180644.jpg"> - </a> - <p> - My niece, HuiHui, my cousin Lin Sen's daughter. - </p> - <a href="/static/media/1920/zy_20190212T202404.jpg"> - <img class="img-center" src="/static/media/720/zy_20190212T202404.jpg"> - </a> - <p> - With my aunt's, uncle Zhu Ju Gong, and cousin Zhu Yun, at the Gondelin - Vegetarian Restaurant on Nan Jing Lu. - </p> - <video controls> - <source src="/static/media/dwrz_20190213T113726_edit.mp4" - type="video/mp4"> - Your browser does not support video. - </video> - <video controls> - <source src="/static/media/dwrz_20190213T171353_edit.mp4" - type="video/mp4"> - Your browser does not support video. - </video> - <video controls> - <source src="/static/media/dwrz_20190213T171856_edit.mp4" - type="video/mp4"> - Your browser does not support video. - </video> - <p> - I love Chinese gardens. They are fractal, meandering, varied, harmoniously - integrating human needs with natural patterns. The nooks and crannies - offering private, intimate space. It's the kind of setting I would love to - work in, every day. - </p> - <a href="/static/media/1920/dwrz_20190213T113948.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190213T113948.jpg"> - </a> - <blockquote> - <p> - <i>In the bamboo grove I delight wise kings;</i> - <br> - <i>A hundred acres of me by the Wei brings fame.</i> - <br> - <i>My green skin is naturally marked by the tears of the Xiang Goddess;</i> - <br> - <i>My scaly shoots pass on the scent of history.</i> - <br> - <i>My leaves will never change their color in frost;</i> - <br> - <i>The beauty of my misty twigs can never be concealed.</i> - <br> - <i>Few have understood me since the death of Wang Huizhi;</i> - <br> - <i>Since ancient times I have been known through brush and ink.</i> - <br> - <br> - Journey to the West, Chapter 64. - </p> - </blockquote> - <a href="/static/media/1920/dwrz_20190213T165429.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190213T165429.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190213T170021.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190213T170021.jpg"> - </a> - <p> - There seems to be a healthy, thriving community of community cats in the - neighborhood, fed by volunteers. Unfortunately, the vast majority of them - do not appear to be spayed. - </p> - <a href="/static/media/1920/dwrz_20190213T170617.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190213T170617.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190213T171237.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190213T171237.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190213T124011.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190213T124011.jpg"> - </a> - <p> - The Song Yuan teahouse outside ZhaBei Park. - </p> - <a href="/static/media/1920/dwrz_20190213T112852.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190213T112852.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190213T173920.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190213T173920.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190213T192724.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190213T192724.jpg"> - </a> - <video controls> - <source src="/static/media/dwrz_20190214T162515_edit.mp4" - type="video/mp4"> - Your browser does not support video. - </video> - <a href="/static/media/1920/dwrz_20190214T163432.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190214T163432.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190214T164630.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190214T164630.jpg"> - </a> - <p> - The aparment, seen from ZhaBei Park. - </p> - <a href="/static/media/1920/dwrz_20190214T190048.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190214T190048.jpg"> - </a> - <p> - The last dinner before departure, again at the Song Yuan teahouse. The - dinner, was 120 RMB, i.e., less than 20 USD. We had leftovers, and a free - rice pudding desert was included with the meal. - </p> - <a href="/static/media/1920/dwrz_20190214T193818.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190214T193818.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190214T211323.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190214T211323.jpg"> - </a> - <p> - All packed up. If I close my eyes now, thousands of miles away, I can still - feel as if I'm there. In the weeks after a return home, the mind struggles - to tell which is real, and which is the dream. The reality on return is - familiar, but a sudden shift from something that felt just as real. - </p> - <a href="/static/media/1920/dwrz_20190214T232759.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190214T232759.jpg"> - </a> - <p> - On the way to the airport, heartbroken but grateful. - </p> - <a href="/static/media/1920/dwrz_20190215T090530.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190215T090530.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190215T054134.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20190215T054134.jpg"> - </a> - <p> - Recollection is the only way the immigrant can ever be at home — in - more than one place at the same time. - </p> -</div> diff --git a/cmd/web/site/entry/static/2019-02-16/metadata.json b/cmd/web/site/entry/static/2019-02-16/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "dwrz_20190205T195842.jpg", - "date": "2019-02-16T00:00:00Z", - "published": true, - "title": "China" -} diff --git a/cmd/web/site/entry/static/2019-03-04/2019-03-04.html b/cmd/web/site/entry/static/2019-03-04/2019-03-04.html @@ -1,65 +0,0 @@ -<div class="wide64"> - <p> - This morning, I dreamed I was in a strange place, a home, and town, built among stairs. - </p> - <a href="/static/media/1920/dwrz_20160118T074711_edit.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20160118T074711_edit.jpg"> - </a> - <p> - In my mind, I was in Hong Kong, and the location was a mix of the escalators and overpasses there; the overall architecture seemed familiar. What was unusual was that the whole place appeared to be in the sky, a neighborhood of building-pods in the air, connected by crisscrossing stairways. In the dream, I was searching for something, people, and a conference I needed to attend. - </p> - <a href="/static/media/1920/dwrz_20160125T082048_edit.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20160125T082048_edit.jpg"> - </a> - <p> - At some point, my alarm went off. I awoke in the dream, at my home in this strange place. Some part of my mind was unsure that I'd actually awoken, and looked for details in the room to reassure that I had, in fact, awoken to reality. Indeed, I found some tell-tale signs – a spot of on the wall of missing paint that's in my current room, the bed was plain wood, just like the one I'd actually fallen asleep on. - </p> - <a href="/static/media/1920/dwrz_20160129T132034_edit.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20160129T132034_edit.jpg"> - </a> - <p> - I got up in the dream, somewhat certain that I'd awoken to reality. The room had floor to ceiling windows, and outside I could see sky and stairs. - </p> - <p> - In the meantime, though, the alarm kept ringing. Soon enough I realized that I'd not actually woken, and suddenly I came to, out of the dream. - </p> - <a href="/static/media/1920/dwrz_20160122T075513_edit.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20160122T075513_edit.jpg"> - </a> - <p> - There were a few things about this waking that I found peculiar. - </p> - <p> - First, the sound of my actual alarm made it through the dream. But it was included in it, the mind wove it into the context, rather than taking it as a sign that the context was false. It was used to reinforce an illusion, rather than dissolve it. - </p> - <p> - Second, some part of the mind was skeptical of waking in the dream, less willing to suspend the disbelief that made a neighborhood with stairs instead of roads plausible. The illusion beginning to crack under something's scrutiny. - </p> - <p> - Third, some other part of the mind coming to the rescue of the illusion. "See?", it says, "here is the spot in your room without paint; of course this is real!" This is the part that puzzles me the most. It is slightly frightening, in a way, an active, and clever, part of the mind, which seeks to keep the rest deceived. - </p> - <a href="/static/media/1920/dwrz_20160122T065930_edit.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20160122T065930_edit.jpg"> - </a> - <p> - I wonder how much of these "programs" are active even in wakeful life: - </p> - <p> - A part of the mind which pre-filters inputs, selecting ones deemed coherent to a narrative, ignoring those which damage it. As if the conscious mind, the supposed manager, was really directed by what the employees decided to report to it. - </p> - <p> - A part of the mind that chooses what we focus on, that crafts a narrative, that perhaps even commands the pre-filtering. A schemer of the senses, an internal manipulator. I wonder why such a mechanism exists, that seeks to keep things cohesive, that wants to suggest continuity and integrity of perception. - </p> - <p> - Finally the skeptic, which seems to sense that it is being hoodwinked, which wants to offer a second glance at what the senses seem to provide it. Suspicious of its fellows. - </p> - <a href="/static/media/1920/dwrz_20160116T085654.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20160116T085654.jpg"> - </a> - <p> - How strange that evolution has favored the emergence of these processes in the mind. I wonder if the actual relationship, or at least the emergent one, is more cooperative than antagonistic. More enmeshed than separated. Overall, the feeling is of being at the mercy of these processes. At the very least, that the conscious self is not so far up the hierarchy as it imagines itself to be. - </p> - <a href="/static/media/1920/dwrz_20160128T180930_edit.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20160128T180930_edit.jpg"> - </a> -</div> diff --git a/cmd/web/site/entry/static/2019-03-04/metadata.json b/cmd/web/site/entry/static/2019-03-04/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "dwrz_20160129T132034_edit.jpg", - "date": "2019-03-04T00:00:00Z", - "published": true, - "title": "Hong Kong Dream" -} diff --git a/cmd/web/site/entry/static/2019-03-18/2019-03-18.html b/cmd/web/site/entry/static/2019-03-18/2019-03-18.html @@ -1,29 +0,0 @@ -<div class="wide64"> - <a href="/static/media/1920/dwrz_20181202T104802.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20181202T104802.jpg"> - </a> - <p> - Over centuries, the domain of philosophy has narrowed. Ancient philosophers articulated theories about the nature of matter, today we accept or reject such theories on the basis of empirical evidence. Aristotle was an ethicist but also a biologist and a psychologist and a political theorist. Today those areas of knowledge on their own are considered general, and are subdivided into various areas of specialization. - </p> - <p> - A core of questions, however, remains in the domain of philosophy, though the mother can now seek the counsel of her daughters. The question: "what is the meaning of life?" does not belong to either biology, neuroscience, or linguistics. It is a purely philosophical question – one that searches for wisdom more than knowledge. But to answer it in the abstract, discounting the progress made in those other fields, is akin to leaping off the shoulders of giants. We have greater visibility into our condition now, more than any other age, and greater introspection, too. - </p> - <p> - Two thousand years ago, if you'd asked an everyday Athenian about the meaning of life, they might have replied that it was to "please the Gods." That, is to please Zeus and the pantheon of Mount Olympus. Today we know for a fact that there are no Gods on Mount Olympus, and the vast majority of modern Athenians no longer believe that Poseidon controls the seas. They get their meteorological forecasts from the weather service, not an oracle. - </p> - <p> - Notwithstanding a more generous interpretation of that response, what can we make of it at face value? Is the meaning of life to please Zeus and Aphrodite? Very few today would find that answer meaningful. To discount knowledge is to distance oneself from wisdom. - </p> - <p> - As far as we can tell today, we inhabit a universe approximately 13 billion years old. The planet we inhabit is a third of that age. Our first ancestors emerged out of chemistry not too long afterwards. We are born, live for an unknown number of decades (if we are lucky), and then we die. We share this with almost all of our brothers and sisters, that is, all things that are carriers of DNA. It seems billions of years of experience have suggested that mortality is a prudent long-term strategy for the success of that molecule. But so is the desire to live. - </p> - <p> - Our portrait of the world is clearer not only in what it includes, but also in what it defines as unclear. There is no empirical evidence of a soul, though conscience and subjective experience is not yet understood as a phenomenon. It is still at the boundaries of our knowledge, as is the origin of the universe itself. - </p> - <p> - Still, we know that the software of the mind runs on much more well-defined set of hardware: neurons and nerves powered by sugars and oxygen. We can group the neurons by functionality, those tasked with processing speech, those which give rise to emotion. Perhaps a subset that handles queries into the meanings of words. - </p> - <p> - We must account for this context, and more. Our focus has narrowed but the bar has been raised. To answer "what is the meaning of life" we have to, if we want a good answer, a satisfying answer, discuss language and questions and the meaning of meaning. We have to explore the feelings that give rise to the asking of that question, and the feelings that arise from the possible responses. And we have to address those feelings, and weave them into not just into the answers but the inquiry itself. For what is sought is not a number, not a fact, not a plot on a graph – but understanding, comprehension, and peace. - </p> -</div> diff --git a/cmd/web/site/entry/static/2019-03-18/metadata.json b/cmd/web/site/entry/static/2019-03-18/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "dwrz_20181202T104802.jpg", - "date": "2019-03-18T00:00:00Z", - "published": true, - "title": "Philosophy in the 21st Century" -} diff --git a/cmd/web/site/entry/static/2019-03-19/2019-03-19.html b/cmd/web/site/entry/static/2019-03-19/2019-03-19.html @@ -1,66 +0,0 @@ -<div class="wide64"> - <a href="/static/media/1920/giordano-bruno-mnemonic.jpg"> - <img class="img-center-small" - src="/static/media/720/giordano-bruno-mnemonic.jpg"> - </a> - <blockquote> - <p> - <i>He inferred that persons desiring to train this faculty (of memory) must select places and form mental images of the things they wish to remember and store those images in the places, so that the order of the places will preserve the order of the things, and the images of the things will denote the things themselves, and we shall employ the places and the images respectively as a wax writing-tablet and the letters written upon it.</i> - <br> - <br> - </p> - <p> - Cicero, <i>De oratore</i>. Traslated by E.W. Sutton and H. Rackham. - </p> - </blockquote> - <p> - On the 11th I dreamed of a house that was a blend of many former homes. A long hallway connected a series of rooms, as was the case in the last apartment in Naples, and the second one in New York. The last bedroom was a combination of a bedroom in Shanghai and one from the apartment in Naples. - </p> - <a href="/static/media/1920/dwrz_20071202T143426.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20071202T143426.jpg"> - </a> - <div class="caption"> - <p> - The master bedroom in Naples, more than a decade after it was vacated. - </p> - </div> - <p> - In the dream, my brother woke me. I had overslept, and was confused. Apparently, I'd missed a flight. As I started to waken (in the dream), I realized I'd not actually missed the flight, but was very late in getting ready for it. A driver was waiting, the image was of a black car outside the apartment complex in Shanghai. It was night, and raining hard. I vaguely recall speaking to the driver on the phone – he was an American, from Boston, though like myself also an immigrant. - </p> - <p> - In the last bedroom, speaking with my parents about the upcoming journey, my mother asked where I would be staying. Would it be at the Roosevelt Hotel – or home? I told her I wanted to stay home. That was the end of the dream. - </p> - <a href="/static/media/1920/dwrz_20110805_65.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20110805_65.jpg"> - </a> - <div class="caption"> - <p> - <i>Curtains in Shanghai</i>. - </p> - </div> - <blockquote> - <p> - <i>If we are not content with our ready-made supply of backgrounds, we may in our imagination create a region for ourselves and obtain a most serviceable distribution of appropriate backgrounds.</i> - </p> - <p> - <i>Rhetorica ad Herrenium</i>. Translated by Harry Caplan. - </p> - </blockquote> - <p> - It was a stereotypical one in many ways – the prominence of architecture, and the compression of architectural features from different places into a single contiguous location. I wonder if these are the mind's attempts to establish continuity, by crafting a consistent signal from many varied ones – a superposition of neural waves. - </p> - <p> - Then, the ambiguity of home. Traveling to a different place, but having the option of staying home. Roosevelt Island – where I've been living since 1994 – designated hotel, not home. - </p> - <a href="/static/media/1920/dwrz_20190314T075645.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190314T075645.jpg"> - </a> - <div class="caption"> - <p> - <i>Roosevelt Island</i>. - </p> - </div> - <p> - On the 16th I dreamed of Montesanto. The weather in March is possibly a trigger, a reminder of travels in Italy and Europe while on Spring Break. Even now, watching the darkening dusk, the memories come flooding back. - </p> -</div> diff --git a/cmd/web/site/entry/static/2019-03-19/metadata.json b/cmd/web/site/entry/static/2019-03-19/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "giordano-bruno-mnemonic.jpg", - "date": "2019-03-19T00:00:00Z", - "published": true, - "title": "Dreams of Home" -} diff --git a/cmd/web/site/entry/static/2019-03-23/2019-03-23.html b/cmd/web/site/entry/static/2019-03-23/2019-03-23.html @@ -1,69 +0,0 @@ -<div class="wide64"> - <video autoplay loop muted - class="video" src="/static/media/dwrz_20100310_6_edit.mp4" - type="video/mp4" style="margin-top: 0;"> - Your browser does not support video. - </video> - <blockquote> - <p> - <i>HEAVEN signifies night and day, cold and heat, times and seasons.</i> - <br> - <i>EARTH comprises distances, great and small; danger and security; open ground and narrow passes; the chances of life and death.</i> - <br> - <br> - Sun Tzu, <i>The Art of War</i>. - </p> - </blockquote> - <p> - I'm sitting in bed watching dusk turn to night. Outside, the wind is gusting hard; inside, stillness. Trees sway while the wind swishes, then thuds. The keyboard clatters quietly as I type. - </p> - <p> - Earlier, I saw photographs of dead baby owls. The wildlife rescuer that shared them suspected the wind was to blame, though they noted that the parents had not been having much luck bringing food back to the nest. The mother was still roosting, a sign that perhaps another chick remained. - </p> - <a href="/static/media/1920/bobby-horvath_20190322.jpg"> - <img class="img-center-small" - src="/static/media/720/bobby-horvath_20190322.jpg"> - </a> - <div class="caption"> - <p> - Photograph by <a href="https://www.facebook.com/bobby.horvath.9/posts/10217122088912000?__xts__%5B0%5D=68.ARCWvxZvniLKhIdT4Av0wv9N1w6rnFO3Z25Oemjv718eEwkYhb4DRdbWC2opxVUHgLQeVGmGXit_EBiONv-bw4-AezdZ6mmNuoboeslxMtXnB_YRqjHUDqqYnisVfjd_tkGaq87uILMg8OWoWzXnpWjO1OPm3VAKANztAey5P2UfbHs9wqy32vv5emQOl29DZo2-rLjt2lxiFdDcj4pOgW8RmA0OZJoM7a8p4xQj86WegdNAShAMQjmse-hIyFW15Zce8BsjqYiPg1oGNjfNLdJWVT7br_Xioh1JW7Sh5wZcZb-rcHOQbnO0Rd0MsxEoHW6duZiQBawUAyEeZiwm0JDLQA&__tn__=C-R">Bobby Horvath</a>, via <a href="https://www.facebook.com/WINORR-Wildlife-In-Need-of-Rescue-and-Rehabilitation-113685721999067/">WINORR</a>. - - </p> - </div> - <p> - I wonder how responsible we are for this wind, for this new, kinetic climate. I, satiated and warm, recall the brief periods of my life where I have felt unending hunger. And my heart breaks for the world. - </p> - <p> - Memories of the School of Infantry surface. Nine years have passed, but I feel like I was there yesterday, shivering on a range, machineguns thudding in the background. Apollo occasionally bestowing mercy, Aeolus wiping it away. - </p> - <p> - I remember: - </p> - <p> - My water freezing at night, and clouds of breath under starlit skies. - </p> - <p> - Snow falling over fire, while crimson tracers pierced the darkness. - </p> - <p> - Rain splattering into my tray of white rice and green peas. - </p> - <p> - Lying prone among pine needles, trying not to fall asleep. - </p> - <p> - Defecating on a hilltop and soaking in the sun, the first warm wind of the year. In the distance, fields of dry grass shimmering golden. - </p> - <a href="/static/media/1920/dwrz_20080616T183048.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20080616T183048.jpg"> - </a> - <p> - Everyone responds to hardship in their own way. Although it has taken time, I have found my days spent living like an animal have become a foundation for compassion and empathy. When I see pigeons huddle in a blizzard, I remember nights spent being cold and exposed, with no alternative. I've come admire their resiliency and respect their patience. It always delights my heart to see their dedication to their chicks, when the warmer months return. - </p> - <p> - I think about the baby owls, and how they got to the ground. - </p> - <p> - What road is it we're on, that's marked by all these milestones of death? - </p> -</div> diff --git a/cmd/web/site/entry/static/2019-03-23/metadata.json b/cmd/web/site/entry/static/2019-03-23/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "dwrz_20080616T183048.jpg", - "date": "2019-03-23T00:00:00Z", - "published": true, - "title": "Nightfall" -} diff --git a/cmd/web/site/entry/static/2019-03-26/2019-03-26.html b/cmd/web/site/entry/static/2019-03-26/2019-03-26.html @@ -1,31 +0,0 @@ -<div class="wide64"> - <a href="/static/media/1920/dwrz_20190325T194749.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190325T194749.jpg"> - </a> - <p> - Today, I went to the Italian Consulate to get a new passport. Unexpectedly, the visit prompted a moment of reflection and recollection.<br> - </p> - <p> - Stepping into the Consulate felt almost like traveling to Italy. The <i>Carabiniere</i> at the door, <i>RAI</i> on the TV, the sound of the language floating in the background, the decor and office furniture – the experience was surreal in its consistency.<br> - </p> - <p> - The interior reminded me of the <a href="https://lascuoladitalia.org/">Guglielmo Marconi school</a>, and memories from my three years there came flooding back. During the interview, the topic of my first years in Naples came up, and those memories followed. The office furniture bubbled up memories of my year abroad in Bologna. I felt a little stunned by it all.<br> - </p> - <p> - I speak Italian with my mother and grandmother nearly every day, read the language regularly, and visited Italy last fall. So I wondered why it was that this experience triggered such a strong reaction.<br> - </p> - <p> - With computer memory, there's the concept of <i>direct access</i> and <i>sequential access</i>. The typical metaphor is that the former is like a book, the latter like a scroll. With a book, you can open up any page and immediately view its contents. With a scroll, you must first unwind the sections preceding the one you are interested in reading.<br> - </p> - <p> - In human terms, perhaps the former is like being able to recall where one was on New Year's Eve, or on September 11, 2001; the latter is more akin to describing a meeting or an accident. One recollection focuses on an instance, the other covers a chronology.<br> - </p> - <aside> - <p> - When people remark that "life has gone by fast", perhaps this is a result of performing a direct access on a small set of memories. Recalling sequences tends to emphasize duration.<br> - </p> - </aside> - <p> - I wonder if in human memory there is another kind of access – a sort of <i>vertical-slicing</i> access, that is the combination of the two. What happens is that <i>durations</i> are accessed directly. It's perhaps akin to remembering Christmastime instead of Christmas, 2000, or remembering springs past instead of March, 1997. In my case, the remembering was of spans of time from across my life – childhood, early adolescence, early adulthood – all in one go.<br> - </p> -</div> diff --git a/cmd/web/site/entry/static/2019-03-26/metadata.json b/cmd/web/site/entry/static/2019-03-26/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "dwrz_20190325T194749.jpg", - "date": "2019-03-26T00:00:00Z", - "published": true, - "title": "New Passport" -} diff --git a/cmd/web/site/entry/static/2019-04-05/2019-04-05.html b/cmd/web/site/entry/static/2019-04-05/2019-04-05.html @@ -1,43 +0,0 @@ -<div class="wide64"> - <a href="/static/media/1920/dwrz_20190328T192239.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190328T192239.jpg"> - </a> - <p> - I have had enough days in nature to know what it is like to long for the simplest comforts of civilization. But the brightest memories are not of those comforts; I have spent enough days in civilization to know the longing for the wilderness. - </p> - <p> - On every return from nature, I am shocked by the physical and spiritual malaise of city life. The litter and noise, the constant, constant sales pitch, omnipresent reminders of rank and status, and people broken in all manner of ways. - </p> - <a href="/static/media/1920/dwrz_20190330T143212.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190330T143212.jpg"> - </a> - <p> - It's strange to return from the world of stone and trees to this one that at times seems even more savage than nature. Here every message is that some thing will give you happiness. But away from the artificial world, the default state seems to be exactly that one. Even when the basic needs are only partially met, the reward seems to be beauty and meaning. Eating simple meals over a cookstove, more joy than at world renowned restaurants. - </p> - <a href="/static/media/1920/dwrz_20190330T151616.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190330T151616.jpg"> - </a> - <blockquote> - <p> - <i>Some vices miss what is right because they are deficient, others because they are excessive, in feelings or in actions, while virtue finds and chooses the mean.</i> - </p> - <p> - Aristotle, <i>Nichomachean Ethics</i>. - </p> - </blockquote> - <p> - Earth supports life because it is exactly the right distance from the Sun at this point in the stellar lifecycle (i.e., within the <a href="https://en.wikipedia.org/wiki/Circumstellar_habitable_zone">circumstellar habitable zone</a>). In a universe where the temperatures ranges from about -272°C to 3,000,000°C, the healthy temperature for a human body is ~36.5-37.5°C. The trick to life has always been <a href="https://en.wikipedia.org/wiki/Homeostasis">balance</a> – not too hot, not too cold, not too much, not too little. Success is not first place, not maximums or minimums, but <a href="https://en.wikipedia.org/wiki/Averageness">averageness</a>. - </p> - <a href="/static/media/1920/dwrz_20190403T170425_edit.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190403T170425_edit.jpg"> - </a> - <p> - There is a point of balance between the state of nature and the technology that we have today. It is possible to build quiet cities that interweave with and respect nature. It is possible to enjoy shelter and agriculture and books while still being able to delight in the hooting of an owl and blinking fields of fireflies. And yes, <a href="https://www.drawdown.org/">it is possible to have the best of civilization without wrecking the foundations on which it rests</a>. - </p> - <p> - But that is not where we are today. What we have instead is a world where millions starve while others drown in the misery of material possessions, where we are safe from the elements but blind to the stars, and where nearly every metric of planetary health is failing. Our planet is sick because our civilization is sick. Our civilization is sick because technology cannot, has not, and will not answer spiritual and psychological dilemmas. In fact, the very things that can, nature and community, are being destroyed by the it. - </p> - <p> - The task before us is moderation – dialing back excess – finding and choosing the mean. - </p> -</div> diff --git a/cmd/web/site/entry/static/2019-04-05/metadata.json b/cmd/web/site/entry/static/2019-04-05/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "dwrz_20190328T192239.jpg", - "date": "2019-04-05T00:00:00Z", - "published": true, - "title": "Homeostasis" -} diff --git a/cmd/web/site/entry/static/2019-04-13/2019-04-13.html b/cmd/web/site/entry/static/2019-04-13/2019-04-13.html @@ -1,39 +0,0 @@ -<div class="wide64"> - <a href="/static/media/1920/dwrz_20110122T220843.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20110122T220843.jpg"> - </a> - <p> - On Thursday, I received news that a Marine from my former Company was KIA in Afghanistan. He was 25 years old. - </p> - <p> - Marine Corps warfighing doctrine emphasizes the psychological level of war. The true objective of combat operations is not to kill the enemy, but to break the enemy's willingness to fight. The former is just one of many means to the latter. - </p> - <p> - The attack on my unit was delivered by means of an SVBIED – a suicide-vehicle-borne improvised-explosive-device. This is one incident, but it seems to underscore this fact: after 18 years on Afghan soil, the United States has failed to undermine the enemy's resolve. - </p> - <p> - A trillion dollars and the most advanced weaponry in the world and 72,000 enemy KIA have failed to convince an <a href="https://en.wikipedia.org/wiki/Least_Developed_Countries">LDC</a> that it can aspire to better. - </p> - <p> - It's possible that numbers alone could have predicted this outcome. Divide the enemy combatants killed by the cost of <a href="https://en.wikipedia.org/wiki/Operation_Enduring_Freedom">OEF</a> and the result is ~14,000,000 USD per head. The Taliban has inflicted only a fraction of its losses on the United States, but probably for orders of magnitude less in costs, too. - </p> - <aside> - <p> - In reality, the Taliban has been even more "efficient", if you consider that they have killed not just ~3,500 <a href="https://en.wikipedia.org/wiki/Coalition_casualties_in_Afghanistan">Coalition</a> troops, but ~60,000 servicemembers in the Afghan Security Forces. - </p> - </aside> - <p> - If we judged wars the way we judged businesses, perhaps the United States would have gotten out of Afghanistan years ago. Perhaps it would never have committed, or been smarter about its engagement, more precise about the problem it was seeking to address. In a way, the country was cursed, as is often the case, by its wealth. A poorer nation wouldn't have been able to afford a blank check. - </p> - <p> - But here we are. A family now mourns the loss of a son. In Afghanistan, there are families that have been mourning loss after loss after loss. Some Afghans fear what will happen when American troops finally depart. And I will never forget the footage of the jumpers on 9/11. - </p> - <aside> - <p> - But who remembers the 4 million people killed by air pollution every year? - </p> - </aside> - <p> - All this in a world where the Arctic ice is evaporating, and Antarctic ice shattering, and life, on the whole, vanishing. I wonder how we will judge this history in twenty or thirty years' time. Sooner or later, we are bound to have a moment of clarity. - </p> -</div> diff --git a/cmd/web/site/entry/static/2019-04-13/metadata.json b/cmd/web/site/entry/static/2019-04-13/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "dwrz_20110122T220843.jpg", - "date": "2019-04-13T00:00:00Z", - "published": true, - "title": "The Cost of War" -} diff --git a/cmd/web/site/entry/static/2019-07-05/2019-07-05.html b/cmd/web/site/entry/static/2019-07-05/2019-07-05.html @@ -1,280 +0,0 @@ -<div class="wide64"> - <p class="poetry"> - The things that one grows tired of—O, be sure<br> - They are only foolish artificial things!<br> - Can a bird ever tire of having wings?<br> - And I, so long as life and sense endure,<br> - (Or brief be they!) shall nevermore inure<br> - My heart to the recurrence of the springs,<br> - Of gray dawns, the gracious evenings,<br> - The infinite wheeling stars. A wonder pure<br> - Must ever well within me to behold<br> - Venus decline; or great Orion, whose belt<br> - Is studded with three nails of burning gold,<br> - Ascend the winter heaven. Who never felt<br> - This wondering joy may yet be good or great:<br> - But envy him not: he is not fortunate.<br> - <br> - <i>Wonder and Joy</i>, Robinson Jeffers<br> - </p> - <p> - During the final days of my travels in Italy, I felt a recurring sadness, which I could not place. It would surface in quieter moments, when I was alone; I would perceive it, and attend to it, but it would not speak its discontents to me. It was present – it wanted me to know that it was present – but otherwise, it was mute and indecipherable.<br> - </p> - <p> - Confused, I tried to give it space, on walks, or at night, meditating over a glass of wine. It was a shy sadness, one that seemed to communicate indirectly, gesturing at memories, using images of the past as metaphor.<br> - </p> - <p> - Its vocabulary was the arc of my life. It spoke to me of my childhood. Together we remembered Christmases at my grandmother's house, the doors with inset glass, behind which lay presents waiting for midnight. Summers at Montesanto: the morning light caught in Naples' rough textures, and echoing church bells interrupted by droning scooters. Walking by the sea at night in Procida – the summer of 1998 – the dark waters brimming with life.<br> - </p> - <p> - Eventually, we made it through the painful years of adolescence and early adulthood, when Naples became a place of mystery and adventure, of undergrounds and tunnels and secret ruins. The chorus of lunches and dinners with family, which restored the spirit as much as the body. Travels with girlfriends, with feelings amplified by the sensuality of the Neapolitan landscape.<br> - </p> - <p> - Then, the recent years, suddenly quieter and more serious, more brooding, less carefree. Remarking the visible passage of time on my own face and that of others, while Naples, old but unchanged, remained like a goddess that outlives her mortal children. Clear-eyed, watching civilization stumble towards downfall and extinction.<br> - </p> - <p> - I wondered if this was the sadness: returning preoccupied to the place of earliest innocence. But I was rarely a cheerful child, certainly not as an adolescent. As a young adult, the inverse: I have found moments of joy and happiness have tended, at least so far, to increase with the years.<br> - </p> - <p> - The last played note of an unheard piece presages those following, but it does not speak for the entirety left unplayed. One story of my life is an awakening to the gloom of the world. But another is an ascension to greater states of gratitude. Yet another is the constant grapple with impermanence, and the unpausing, unwavering march of time.<br> - </p> - <p> - The travels themselves, up to the origin of the sadness, had been mostly stressful. I was tired, burnt-out. Europe was in the midst of a heatwave, and rather than rest and think, I moved around a lot. But I began to recall that there had also been many glimmering moments. Trees swaying in the wind, which wafted the scent of jasmines. Insects – vanishing at unprecedented rates around the world – here flourishing, busy with the work of life. Stars treading the Milky Way, the sound of waves withdrawing from the shore. The voices of family and friends – many which I've known for as long as I've been alive – recounting the recent past.<br> - </p> - <p> - Beneath the stress and exhaustion, there had been many unappreciated moments of delight. Reflecting on that delight brought me joy, but that joy in turn brought me sorrow: a departure was coming that entailed separation from all these things. A separation that echoed the first one, in 1994.<br> - </p> - <p> - A filling moon foretells imminent wane. For every return home there's a farewell and a return to another home, a reminder that my heart is forever split across the world. Every choice to be in one place comes at the exclusion of another. All that can be lived is one portion at a time, so every return becomes a joy reborn, a death anticipated.<br> - </p> - <a href="/static/media/1920/dwrz_20190624T204814.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190624T204814.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190624T205910.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190624T205910.jpg"> - </a> - <video controls> - <source src="/static/media/dwrz_20190624T205926.mp4" type="video/mp4"> - Your browser does not support video. - </video> - <a href="/static/media/1920/dwrz_20190624T214020.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190624T214020.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190624T225352.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190624T225352.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190624T234129.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190624T234129.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190625T090303.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190625T090303.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190625T134207.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190625T134207.jpg"> - </a> - <video controls> - <source src="/static/media/dwrz_20190625T193345.mp4" type="video/mp4"> - Your browser does not support video. - </video> - <a href="/static/media/1920/dwrz_20190625T170112.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190625T170112.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190625T214147.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190625T214147.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190625T234412.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190625T234412.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190626T090328.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190626T090328.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190626T090350.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190626T090350.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190628T125816.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190628T125816.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190628T194218.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190628T194218.jpg"> - </a> - <a href="/static/media/1920/vm_20190629.jpg"> - <img class="img-center" src="/static/media/720/vm_20190629.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190629T180226.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190629T180226.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190629T182152.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190629T182152.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190629T185009.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190629T185009.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190629T185352.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190629T185352.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190629T185642.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190629T185642.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190629T201744.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190629T201744.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190629T201916.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190629T201916.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190629T213153.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190629T213153.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190629T220421.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190629T220421.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190629T221104.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190629T221104.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190629T224248.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190629T224248.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190629T235430.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190629T235430.jpg"> - </a> - <video controls> - <source src="/static/media/dwrz_20190629T235614_edit.mp4" type="video/mp4"> - Your browser does not support video. - </video> - <video controls> - <source src="/static/media/dwrz_20190629T235948_edit.mp4" type="video/mp4"> - Your browser does not support video. - </video> - <a href="/static/media/1920/dwrz_20190630T144416.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190630T144416.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190630T204502.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190630T204502.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190630T224852.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190630T224852.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190630T213237.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190630T213237.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190630T224629.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190630T224629.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T124018.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T124018.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T124344.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T124344.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T124435.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T124435.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T125811.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T125811.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T130801.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T130801.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T180050.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T180050.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T180112.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T180112.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T180126.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T180126.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T180210.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T180210.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T180259.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T180259.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T180527.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T180527.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T180713.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T180713.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T181055.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T181055.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T184312.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T184312.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T193432.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T193432.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T200809.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T200809.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T200824.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T200824.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190701T200844.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190701T200844.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190702T153928.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190702T153928.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190702T195643.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190702T195643.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190702T200509.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190702T200509.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190702T201804.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190702T201804.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190702T201906.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190702T201906.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190702T201911.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190702T201911.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190702T202425.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190702T202425.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190702T202629.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190702T202629.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190702T202701.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190702T202701.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190702T203155.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190702T203155.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190702T204552.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190702T204552.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190702T223316.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190702T223316.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190702T223326.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190702T223326.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190703T155913.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190703T155913.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190703T155917.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190703T155917.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190703T202712.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190703T202712.jpg"> - </a> - <video controls> - <source src="/static/media/dwrz_20190703T202930_edit.mp4" type="video/mp4"> - Your browser does not support video. - </video> - <a href="/static/media/1920/dwrz_20190703T225545.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190703T225545.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190704T064804.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190704T064804.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190704T091923.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190704T091923.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190704T180801.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190704T180801.jpg"> - </a> -</div> diff --git a/cmd/web/site/entry/static/2019-07-05/metadata.json b/cmd/web/site/entry/static/2019-07-05/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "dwrz_20190702T201906.jpg", - "date": "2019-07-05T00:00:00Z", - "published": true, - "title": "Italy" -} diff --git a/cmd/web/site/entry/static/2020-07-23/2020-07-23.html b/cmd/web/site/entry/static/2020-07-23/2020-07-23.html @@ -1,124 +0,0 @@ -<div class="wide64"> - <p> - Dear Assembly Member Seawright, and Borough President Brewer, - </p> - <p> - I attended the Southpoint Park tour on Friday and wanted to follow - up on the concerns I expressed to you there. - </p> - <p> - I understand the importance of repairing the sea wall. But I want - to share why this patch of invasive plants, resting on - contaminated soil, is so important to me. - </p> - <p> - I've been a resident of Roosevelt Island since 1994, since I - emigrated from Naples, Italy. Growing up here was special. My - childhood memories include ladybugs and hummingbirds, watching - cormorants and seagulls fish, and the southern tip of the Island, - when it was still green. As a child, all of these were sources of - joy, delight, and curiosity. Later, they often prompted feelings - of gratitude, and appreciation for the gift of life. - </p> - <p> - Over my lifetime, I have seen most of these sources vanish. I have - seen one hummingbird in the last decade, maybe a handful of - ladybugs. I understand that there were compelling reasons, - sometimes, to develop the Island. But I still mourn the losses I - have witnessed. - </p> - <p> - I visit Southpoint Park several times a week. I've been there in - all seasons, in all kinds of weather, at dawn, afternoon, and - dusk. It is perhaps the one place left where I can still - experience the delights I remember as a child. This summer, it was - watching red sparrows eat mulberries, or the rainbow after a storm - caught me reading in the park. In 2014, I studied for the bar - exam there. When my grandmother suffered a heart attack last year, - I went to Southpoint Park to collect my thoughts. - </p> - <p> - I know that, in days to come, I will walk to that park and find - its shores bare. In place of the maze of green branches will be - overturned dirt, machinery, and views of man-made structures on - the horizon. Even now, I dread to see that view. - </p> - <p> - While I have watched this reverse alchemy of the Island -- of - turning emerald to stone -- I have also learned more of our - planet's situation. It is a fact, today, that the ecological - foundations of human society are buckling. I am wondering when we - will start to turn things around, and if governments' approach to - nature can shift in time. - </p> - <p> - RIOC's current vision for the park entails a loss of twenty trees. - This number hides the fact that some are mature trees, whose - equivalent cannot be planted. There will also be a net loss in - terms of square feet left to nature. This outcome, better than the - original proposal, is only because residents stepped up. - Otherwise, we would have been facing a greater loss. - </p> - <p> - I would love to see RIOC commit to, at the least, maintaining an - equivalent number of trees in the park. I would love for more - space to be left wild. More than that, I would love to see RIOC - match residents' passion and appreciation for the nature we have - here. - </p> - <p> - If you can help to bridge this gap, I will be indebted to you. - </p> - <p> - Thank you very much for your time, - </p> - <p> - David Wen Riccardi-Zhu</br> - 555 Main Street - </p> - <a href="/static/media/1920/dwrz_20180604T185738_edit.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20180604T185738_edit.jpg"> - </a> - <a href="/static/media/1920/dwrz_20180702T194515.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20180702T194515.jpg"> - </a> - <a href="/static/media/1920/dwrz_20181202T120957.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20181202T120957.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190302T170654.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190302T170654.jpg"> - </a> - <a href="/static/media/1920/dwrz_20190922T172424.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20190922T172424.jpg"> - </a> - <a href="/static/media/1920/dwrz_20191110T154821.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20191110T154821.jpg"> - </a> - <a href="/static/media/1920/dwrz_20200321T145850.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20200321T145850.jpg"> - </a> - <a href="/static/media/1920/dwrz_20200414T182733.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20200414T182733.jpg"> - </a> - <a href="/static/media/1920/dwrz_20200512T221837.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20200512T221837.jpg"> - </a> - <a href="/static/media/1920/dwrz_20200512T222426.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20200512T222426.jpg"> - </a> - <a href="/static/media/1920/dwrz_20200525T222228.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20200525T222228.jpg"> - </a> - <a href="/static/media/1920/dwrz_20200528T220022.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20200528T220022.jpg"> - </a> - <a href="/static/media/1920/dwrz_20200530T221922.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20200530T221922.jpg"> - </a> - <a href="/static/media/1920/dwrz_20200629T223151.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20200629T223151.jpg"> - </a> - <a href="/static/media/1920/dwrz_20200701T225232.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20200701T225232.jpg"> - </a> -</div> diff --git a/cmd/web/site/entry/static/2020-07-23/metadata.json b/cmd/web/site/entry/static/2020-07-23/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "dwrz_20181202T120957.jpg", - "date": "2020-07-23T00:00:00Z", - "published": true, - "title": "On the Loss of Southpoint Park" -} diff --git a/cmd/web/site/entry/static/2020-10-08/2020-10-08.html b/cmd/web/site/entry/static/2020-10-08/2020-10-08.html @@ -1,58 +0,0 @@ -<div class="wide64"> - <a href="/static/media/1920/dwrz_20200926T110433_edit.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20200926T110433_edit.jpg"> - </a> - <blockquote> - <p>I asked the leaf whether it was frightened because it was autumn and the other leaves were falling. - <br> - <br> - The leaf told me, "No. During the whole spring and summer I was completely alive. I worked hard to help nourish the tree, and now much of me is in the tree. I am not limited by this form. I am also the whole tree, and when I go back to the soil, I will continue nourish the tree. So I don’t worry at all. As I leave this branch and float to the ground, I will wave to the tree and tell her, 'I will see you again very soon.'" - </p> - <br> - <cite>Thích Nhất Hạnh, The Heart of Understanding</cite> - </blockquote> - <p>At 33, I've found my recollection of the past has started to change. The moments longed for are frequently more than a decade gone. Frolicking in the Paris Catacombs, that was the summer of 2006. The year abroad in Bologna -- 2007 and 2008. Shooting rockets at dusk, that was 2010.</p> - <p>I remember wandering to these memories when they were just a few years old. It's strange to see them now so far away, like landmarks receding into the horizon.</p> - <a href="/static/media/1920/dwrz_20200906T223917_edit.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20200906T223917_edit.jpg" - alt="New York City, seen from West Mountain."> - </a> - <p>I can distinguish chapters now. Middle school, high-school, college, the Marine Corps, law school, three years working in policy, the transition to software engineering, then the journey with Good Uncle. The story itself still doesn't make much sense to me. Sometimes I can detect the faint pulse of a purpose, like a thread that's been stitched, but not yet pulled taught. Other times, I'm not sure if I'm sensing so much as imagining that pulse.</p> - <a href="/static/media/1920/dwrz_20200926T114004_edit.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20200926T114004_edit.jpg"> - </a> - <p>I call my grandmother almost every day. She asks me about my health and tells me it's the most important thing. "Solo quando si sta bene si lotta" -- only when one is well can one fight -- through life's difficulties. She speaks from experience, I know. 55 more years of it.</p> - <p>Only 33, I can still tell that the "oomph" has started to wane. Slight but detectable. I loved hard workouts before; now I appreciate them, but it requires a little more coaxing. Jamie, my wushu instructor, spent his twenties as an acrobat in the Beijing Opera. At thirty, he too could notice the beginnings of physical decline. For him, the rate was steady until about 50. Then, it accelerated.</p> - <a href="/static/media/1920/dwrz_20200926T112953_edit.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20200926T112953_edit.jpg"> - </a> - <p>I wonder why we are wired to take things for granted. Health and vigor are invaluable treasures for those lacking them, but not sufficient to satisfy their possessors. Was it less so in a world less privileged, where suffering was more visible? When more of us were poor, oppressed, incurably sick -- did gratitude come any easier?</p> - <p>Are we wired for ingratitude, or is our jaded state attributable to unnatural circumstances? Diabetes, obesity, and insomnia are more common in the developed world -- are psychological dispositions also so? Privilege hides the suffering of others -- factory farms, children mining rare earth minerals, police brutality -- things constant in the world are reduced to momentary trespassers in our conscience.</p> - <p>This blindness hurts us, too. It hurts us because inevitably these evils will catch up to us -- in the form of rising seas, shattered storefronts, and depression caused by addiction to technology. It hurts us because we pass over the beauty that is there, already in the world. Take an ancestor from a few centuries back and show them a supermarket, or a hospital, or video calls. They would shed tears of joy. Do you understand the privilege of not being hungry? We are fortunate beyond our ancestors' wildest dreams, and yet their delight is our indifference.</p> - <a href="/static/media/1920/dwrz_20200926T132417_edit.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20200926T132417_edit.jpg"> - </a> - <p>At 33, I've started to gleam this lesson, that happiness is found primarily in gratitude, moreover that one can exercise this state of mind. I look over my photographs every Sunday, and it's hard to ever come away without some sense of amazement. This -- all this -- happened to me? So many faces and places lost in time, loves faded, friends separated by distance, the donning and doffing of different costumes, and sights so beautiful they still capture my breath.</p> - <p>I find, in this gratitude, love for the world. This universe, the nebulas, star wombs which we were never meant to see -- the electromagnetic radiation that the sun bathes the world in, which cells in my eye detect and encode into visions of green and blue and pink and orange -- and in these cells a double helix, my bond to all living things, trees and birds and hornets, for even the grass that I walk over is a distant sibling.</p> - <a href="/static/media/1920/dwrz_20200926T093938_edit.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20200926T093938_edit.jpg"> - </a> - <p>Did it have to be this way? Is the world we inhabit as inevitable as geometry -- or is there a certain amount of magic that's made it possible?</p> - <p>Thirty-three orbits in observation. I feel very lucky, as if all I need to see fortune's smile is remember to look for her -- and she is everywhere, in every thing.</p> - <a href="/static/media/1920/dwrz_20200926T114327_edit.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20200926T114327_edit.jpg"> - </a> - <p>But somehow, I don't always remember. For every day of gratitude there seem to be at least a few of anger, heartbreak, and drudgery. It's hard to accept the suffering in the world as beautiful design. The intentional injustice, so pervasive, eclipses even the most spectacular of sunsets. I wonder if time will bring acceptance of these things. Right now, I'm not so sure.</p> - <p>These days, it feels like we're in the autumn of human civilization. The arc of the universe bends towards justice, but perhaps we're now on the other side of the parabola. Things are falling apart, fast, and I wonder if the four figures on the horizon are horsemen.</p> - <a href="/static/media/1920/dwrz_20200926T112127_edit.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20200926T112127_edit.jpg"> - </a> - <p>I simply don't know if the equation can be balanced. A rocket either has or does not have the energy required to reach and maintain escape velocity. If it does, orbit is an option, if it does not, it might go high into the sky, but eventually tumble back towards Earth. I don't know if our civilization's goals are attainable. If they are, then the problem is alignment on action, which is not a given. If they are not, then it might be time to reassess these goals.</p> - <p>Some afternoons ago, watching the tide swell at dusk, I felt a mandate coalesce, then surface: "repair the circle of life". History is full of men that have dreamed of wealth, power, and conquest. I am utterly disinterested in these things, which slip through our hands like water. But to live in a world where biodiversity is increasing, where the populations are coming back from the brink, regenerating -- what I would give, to make it real.</p> - <video controls> - <source src="/static/media/dwrz_20200926T180908.mp4" type="video/mp4"> - Your browser does not support video. - </video> -</div> diff --git a/cmd/web/site/entry/static/2020-10-08/metadata.json b/cmd/web/site/entry/static/2020-10-08/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "dwrz_20200926T110433_edit.jpg", - "date": "2020-10-08T00:00:00Z", - "published": true, - "title": "Autumn" -} diff --git a/cmd/web/site/entry/static/2020-10-30/2020-10-30.html b/cmd/web/site/entry/static/2020-10-30/2020-10-30.html @@ -1,85 +0,0 @@ -<div class="wide64"> - <a href="/static/media/1920/dwrz_20120708_64.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20120708_64.jpg"> - </a> - <blockquote> - <p>When kissing one's child, one should, said Epictetus, say to oneself: "You will perhaps die tomorrow." Ill-omened words, these! "No word is ill-omened", he - said, "which signifies a natural process. Else it would have be ill-omened to - say that the wheat has been harvested."</p> - <br> - <cite>Marcus Aurelius, Meditations.</cite> - </blockquote> - <p>On the morning of October 7, on my way to the Roosevelt Island pool, I came across a belted kingfisher flailing on the ground. It was unable to move, except by dragging its body, and looked terrified by this newfound vulnerability.</p> - <a href="/static/media/1920/dwrz_20201007T123414_edit.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20201007T123414_edit.jpg"> - </a> - <p>I did not know what to do. But others started to walk past, and then a few cars rumbled along the road; then, I knew I could not leave. As I picked him up -- he was a male -- I could feel his heartbeat accelerate, strikingly powerful in such a light body. My heart too, began to beat a little faster.</p> - <p>I contacted the <a href="https://www.wildlifefreedomfoundation.org/">local wildlife rehabilitator</a> and asked her if I could bring him in. When she said yes, I started walking back home, holding the bird gently against my chest. To my surprise, his heartbeat settled -- until we passed our first dog. Then, he began to squirm, and again I could feel the drumming in my hands. When the dog went out of sight, he was calm again.</p> - <p>This happened a few more times. It was a sunny morning, and many were out for their first stroll. Each time we began to draw near a dog, I could feel the kingfisher's heart beat faster -- and out of concern for him, so did mine. I was struck by this bond, a common language. Millions of years of evolutionary history separated us -- but I knew what he was feeling: fear.</p> - <a href="/static/media/1920/dwrz_20201007T123545_edit.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20201007T123545_edit.jpg"> - </a> - <p>When I got home, I put him in a padded basin. I'd never seen a kingfisher on the Island before, much less this closely. He was beautiful, beyond anything a human hand could make. Silver-blue, with a stone colored beak, and a flamboyant crest.</p> - <p>I brought him to the rehabilitator, who lives next door, and left him in her care. Together we wondered about what had happend to him. With no visible injuries, we guessed blunt trauma. He'd either struck a window and dragged himself to where I found him, or the impact was with a vehicle on the Queensboro Bridge.</p> - <p>I abandoned my plans to swim that morning, and went back home, to work. I felt like I'd just gotten off a roller-coaster. Throughout the day, I wondered whether he'd manage to pull through. The next day, I checked with the rehabilitator, and she told me he was not faring well -- not eating, not moving much. When I checked again a few days later, she told me he hadn't made it. He was gone.</p> - <a href="/static/media/1920/dwrz_20201007T123646_edit.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20201007T123646_edit.jpg"> - </a> - <p>It's difficult to capture this loss with words. This was an animal I did not know, and our shared journey was less than a half-hour long. But I was -- I am still -- heartbroken. Why?</p> - <p>I wonder how much of the difficulty I can attribute to its symbolism. I pass by dead pigeons regularly -- one today -- and while I feel a sense of loss -- it's easier to accept. That's the course of life, inevitable. But to have come across something spectacular as it lay dying -- that hits close to home. That is the story of my generation. We have come into a beautiful world, and found it flailing <a href="https://www.hugomichellgallery.com/portfolio/narelle-autio/indifference/">on the side of a road</a>.</p> - <p>It seems common, at least in the northern hemisphere, to associate this period of the year with mourning. Last Sunday, it was the Double Ninth festival in China, where it is tradition to visit ancestors' graves. The coming Sunday is <em>Tutti i Santi</em> -- All Saint's Day -- where the same tradition is observed in Italy. So I find myself, in the wake of this loss, in this season of mourning, reflecting on the year's departures.</p> - <a href="/static/media/1920/dwrz_20200321T144854_edit.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20200321T144854_edit.jpg"> - </a> - <p>On March 21, walking through Southpoint Park, I came across one of our community cats, Cremina, who was yowling. I went to console her, and she settled down -- into my lap -- and we enjoyed the returning sun. She was sick -- and something like seventeen years old. During her remaining weeks, I went to help with her care, and she would nap in my lap for a good half-hour, waking to purr here and there. I wasn't there for her passing, on April 15, and the regret lingers.</p> - <a href="/static/media/1920/dwrz_20200408T170558_edit.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20200408T170558_edit.jpg"> - </a> - <p>On May 7, walking with my mother around Roosevelt Island, we passed the Octagon Gardens, where I fed the community cats from 2015 to 2018. Two notables from that crew were Tom and Candy, who were inseparable. That day, too, they were sitting next to each other, probably waiting for their evening meal. But something was different.</p> - <a href="/static/media/1920/dwrz_20170831T183326_edit.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20170831T183326_edit.jpg"> - </a> - <p>Candy had a huge tumor on her nose, which had disfigured her, nearly blocking half her left eye. This was the feeling: you're good friends with someone, you see them for three years, in all kinds of weather -- snow and rain, summer sunsets, falling leaves. They come to expect your arrival. Then life separates you, until you run into them on the street someday, and they confess -- although it's evident anyway -- that they are a terminal case.</p> - <a href="/static/media/1920/dwrz_20200507T224108_edit.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20200507T224108_edit.jpg"> - </a> - <p>I asked Candy, "what happened?" -- and she slowly blinked at me, then looked away, as if embarrassed, acknowledging. That night, I inquired into her condition. I was told that she needed to be put down, but that, because of the pandemic, donations had dropped and there wasn't a budget for it. I, conflicted but following my intuition, offered to take care of it.</p> - <p>I read up on nasal tumors in cats, and set up an appointment with the vet. The next day, May 8, I borrowed a carrier and went with another volunteer to catch Candy. We coaxed her with food. She seemed in good spirits, and went to eat with appetite. Suddenly, I wasn't sure about my timing. But for better or worse, I stuck with the plan.</p> - <p>When I grabbed Candy to put her into the carrier, she struggled, and the tumor burst open. Crimson streaked onto the insides of the carrier. The left side of her nose, now mangled, was moist with blood and pus.</p> - <p>Then, I discovered I'd forgotten my wallet at home. I had to walk with the other volunteer back home, with Candy in the carrier. I hated every minute that I added to her suffering.</p> - <p>Finally, we went to the vet. We waited outside, and it started to rain. I looked at her -- and could tell she was hurting -- bearing her pain with the stoicism that only animals are capable of. A young woman exited the office in tears, devastated. It seemed like the whole world was mourning. The only comfort came at the end, the minute we had when she was asleep, before the injection. She looked at peace, resting lightly.</p> - <a href="/static/media/1920/dwrz_20200508T163756_edit.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20200508T163756_edit.jpg"> - </a> - <p>Afterwards, I kept thinking about how I'd seen her eating only an hour earlier. Was it too early? But if it had been done earlier -- before the tumor had grown so large -- it wouldn't have been as messy. She would have suffered less. Then again -- who was I to decide this creature's fate? Who was I to take away Tom's companion?</p> - <a href="/static/media/1920/dwrz_20100309T152226.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20100309T152226.jpg"> - </a> - <p>During my last year at university, I had already begun to prepare for my enlistment in the Marine Corps Infantry. I knew the experience would be psychologically demanding, even in the best case. The darker end of the spectrum included the possibility of my own death, and the taking of the life of another.</p> - <p>Anticipating those possibilities, I read two of Lt. Col. Dave Grossman's books: <em>On Killing</em>, and <em>On Combat</em>, which explore the inner journeys that potentially fall on members of the warrior caste. Things like survivor's guilt, helplessness in the face of horror, and the psychological stages of killing in combat: exhilaration, remorse, rationalization, and acceptance.</p> - <p>But it's one thing to read a map and another to walk on roads marked by it. My unit was never deployed. There was no trigger pulling, except at paper targets. All I have are blundered attempts to do the "right thing" in an emptying world. No moral clarity. The cats that I have rescued live off the <a href="https://weanimalsmedia.org/hidden/">canned remains of caged birds</a>. Like combat, there aren't so much wins as degrees of devastation.</p> - <a href="/static/media/1920/dwrz_20200804T231801_edit.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20200804T231801_edit.jpg"> - </a> - <p>We have lost something like a hundred trees on Roosevelt Island this year. The vast part were felled by chainsaws and bureaucrats. A smaller part by highly kinetic storms, turbocharged by climate change. Walking around after Tropical Storm Isaias, I came across a sparrow sleeping on the ground, alone. She looked exhausted, and let me sit next to her for a few minutes. When she flew away, I wondered where her tribe was, and what it must have been like to bear this storm outdoors.</p> - <a href="/static/media/1920/dwrz_20200804T230813_edit.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20200804T230813_edit.jpg"> - </a> - <p>Every adolescent goes through Siddhartha's awakening, the realization that we grow old, grow sick, and eventually die. The child's fairy tales yield to recountings of our brutal history. On the personal level, I can see a path to acceptance, even though I'm not there yet. But I don't know how one comes to term with the smirking, shrugging murder of entire ecosystems.</p> - <p>The internet is a young place, run by a generation that, in the aggregate, has not experienced much loss. Time is catching up to us, though. Perhaps the silver lining is that difficulty will breed introspection, which in turn will blossom into maturity. In coming to terms with our own impermanence, I hope we will begin to recognize that meaning cannot be found in likes and the perpetual scroll of delight, physical or virtual. We are caretakers, guardians, of something much bigger than us, and much more enduring. It is only in returning to the world that we can give our lives meaning.</p> - <div class="video-container"> - <iframe src="https://www.youtube.com/embed/EaI-4c92Mqo" allowfullscreen> - </iframe> - </div> -</div> diff --git a/cmd/web/site/entry/static/2020-10-30/metadata.json b/cmd/web/site/entry/static/2020-10-30/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "dwrz_20120708_64.jpg", - "date": "2020-10-30T00:00:00Z", - "published": true, - "title": "Traveling through the Dark" -} diff --git a/cmd/web/site/entry/static/2021-02-06/2021-02-06.html b/cmd/web/site/entry/static/2021-02-06/2021-02-06.html @@ -1,8 +0,0 @@ -<div class="wide64"> - <a href="/static/media/1920/dwrz_20100319T061649_edit.jpg"> - <img class="img-center" - src="/static/media/720/dwrz_20100319T061649_edit.jpg"> - </a> - <p>We are often scared of sacrifice, but I have found it to be the most certain path to fulfillment and joy. The reverse is that, if you have a sense of purpose, the consumptive delights become irrelevant. A person with a mission has an internal source of happiness, or has found something more important than their own happiness. For many veterans, the memories that glow aren't the ones of supreme ease or fine dining, but of being in the field, surrounded by friends, working towards a common cause.</p> - <p>What I would give to make that cause our planet and our future, and to render an entire generation invulnerable to the temptations of luxury, consumption, and status.</p> -</div> diff --git a/cmd/web/site/entry/static/2021-02-06/metadata.json b/cmd/web/site/entry/static/2021-02-06/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "dwrz_20100319T061649_edit.jpg", - "date": "2021-02-06T00:00:00Z", - "published": true, - "title": "Planetary Purpose" -} diff --git a/cmd/web/site/entry/static/2021-02-24/2021-02-24.html b/cmd/web/site/entry/static/2021-02-24/2021-02-24.html @@ -1,31 +0,0 @@ -<div class="wide64"> - <a href="/static/media/1920/dwrz_20110710_2.jpg"> - <img class="img-center" src="/static/media/720/dwrz_20110710_2.jpg"> - </a> - <blockquote> - <p>The things they carried were largely determined by necessity.</p> - <br> - <cite>Tim O'Brien, - <a href="/static/the-things-they-carried.pdf">The Things They Carried</a> - </cite> - </blockquote> - <p>Some of the most valuable lessons in life, I've learned from living out of a pack, for work and for pleasure.</p> - <p>A pack has limited space; the typical bathtub has more volume. The first lesson for anyone heading outdoors is prioritization in the face of scarcity. There is only so much you can take, and the essentials will leave little room for luxuries.</p> - <p>The real problem is weight. Every thing you add to the pack, you add as haul for the journey. Your agility and endurance is negatively correlated to your pack's weight; the heavier the pack, the slower you move, the less distance you can cover. And if you are traveling for pleasure, your joy and the contents of your pack are to a degree at odds. Light shoulders make for pleasant hiking.</p> - <p>Every decision to add something to a pack will be cross-examined by the future self, typically late in the day, on a strenuous climb. One learns to hate the weight of the few big things just as much as the sum of many little things. The accumulated regrets transform into intuitions on necessity. These intuitions carry over to other areas of life.</p> - <p>For example, when it comes to software, every feature takes up space in the pack and represents weight borne somewhere. Early on, the constraints were imposed in terms of memory, computation, bandwidth, storage. But for a while now, the real limitations have been human: labor hours, or the mental capacities of the engineers, who have to carry the code.</p> - <p>The same goes for our planet. The paradigm of modern life in consumptive societies is to fill the pack. We have more space for more stuff than most of our ancestors could ever dream of. But something still has to carry the weight. To a degree, we do -- with our time, money, and attention. Most of burden, though, has been off-loaded to natural systems, which are crumbling underneath the weight of it all.</p> - <p>Usually, when I return from a longer outing, I find myself a little more grateful for civilization. Things I normally take for granted -- plumbing and shelter and flat surfaces to sleep on -- sparkle like magic. When it comes to all the other frosting on the cake, all the other things in the pack, I'm not so sure. I find myself wishing we didn't have to carry it. Maybe without it, our footsteps would be lighter, and the going, easier.</p> - <div class="video-container"> - <iframe src="https://www.youtube-nocookie.com/embed/pCLzMDtUZmI?start=520" - allowfullscreen> - </iframe> - </div> - <blockquote> - <p><i> - You're humping too much stuff, troop. - <br> - You don't need half this shit. - </i></p> - </blockquote> -</div> diff --git a/cmd/web/site/entry/static/2021-02-24/metadata.json b/cmd/web/site/entry/static/2021-02-24/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "dwrz_20110710_2.jpg", - "date": "2021-02-24T00:00:00Z", - "published": true, - "title": "Pack Weight" -} diff --git a/cmd/web/site/entry/static/2022-07-08/2022-07-08.html b/cmd/web/site/entry/static/2022-07-08/2022-07-08.html @@ -1,159 +0,0 @@ -<div class="wide64"> - <p> - 2022年07月08日我和梅根去远足和露营。 - 那天中午我们离开了罗斯福岛;梅根开车带我们到熊山附近,小径开始的地方。 - 由于交通拥挤,我们花了两个小时左右才到那里。 - 我们在小径起点停了车,之后我们开始远足。 - </p> - <a href="/static/media/1920/dwrz_20220708T140451.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T140451.jpg"> - </a> - <p> - 我们走了蓝色的小径,叫“Timp Torne”小径。 - 一开始树木非常茂盛,路平坦,但很快小径就开始爬升了。 - 其实,我们发现这条小径有点难:它非常崎岖,天气也炎热潮湿。 - 而前一天,我也骑了自行车,而梅根跑步跑了四英里。 - </p> - <a href="/static/media/1920/dwrz_20220708T140954.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T140954.jpg"> - </a> - <p> - 尽管如此,小径很美:地形非常多样化,提供了许多景点。 - 我们也发现野生蓝莓,它们小但很好吃! - </p> - <a href="/static/media/1920/dwrz_20220708T143253.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T143253.jpg"> - </a> - <a href="/static/media/1920/dwrz_20220708T155402.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T155402.jpg"> - </a> - <a href="/static/media/1920/dwrz_20220708T152720.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T152720.jpg"> - </a> - <p> - 30分钟远足后,我们到达了一个洞穴,我从没见过像这样的洞穴,很有意思。 - 里面很凉爽,有一只小青蛙。 - </p> - <a href="/static/media/1920/dwrz_20220708T144058.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T144058.jpg"> - </a> - <p> - 我们又远足了一段路,停下来休息,吃梅根最喜欢的<a href="https://schmackarys.com/" target="_blank">饼干</a>,最后我们到达了一个山叫“The Timp”(手鼓山,332米高)。 - 那里有三个方向的美丽景观。 - </p> - <a href="/static/media/1920/dwrz_20220708T164202.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T164202.jpg"> - </a> - <a href="/static/media/1920/dwrz_20220708T164948.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T164948.jpg"> - </a> - <p> - 向南你可以看到哈德逊河和远处的纽约市。 - 向西你可以看到“Harriman”州立公园的绿色的山,西山和西山的庇护所。 - 向北你可以看到哈德逊河,熊山,熊山桥,熊山火塔。 - </p> - <a href="/static/media/1920/dwrz_20220708T165027.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T165027.jpg"> - </a> - <a href="/static/media/1920/dwrz_20220708T164504.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T164504.jpg"> - </a> - <a href="/static/media/1920/dwrz_20220708T170351.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T170351.jpg"> - </a> - <p> 其实,我们觉得地方如此美,我们想在那里搭帐篷。 - 然而,停下来太早了,所以休息一会儿以后,我们继续向西山走。 - </p> - <a href="/static/media/1920/dwrz_20220708T173250.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T173250.jpg"> - </a> - <a href="/static/media/1920/dwrz_20220708T173854.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T173854.jpg"> - </a> - <a href="/static/media/1920/dwrz_20220708T174511.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T174511.jpg"> - </a> - <p> - 我们到达了西山(382米高)汗流浃背,但很开心。 - </p> - <a href="/static/media/1920/dwrz_20220708T174928.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T174928.jpg"> - </a> - <p> - 我们休息一会儿,之后做晚饭。 - 我们吃白豆炖羽衣甘蓝,和用余烬烤的玉米棒。 - 我也吃了一些野生桑葚。 - 不幸,空气有点雾霾,所以我们看不到远处的纽约市。 - </p> - <a href="/static/media/1920/dwrz_20220708T184434.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T184434.jpg"> - </a> - <a href="/static/media/1920/dwrz_20220708T195728.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T195728.jpg"> - </a> - <a href="/static/media/1920/dwrz_20220708T203146.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220708T203146.jpg"> - </a> - <p> - 过了一会儿,另一个徒步旅行者到达,他有一把小吉他,告诉我们他是个初学者。 - 尽管如此,听他弹吉他很好听。 - </p> - <video controls> - <source src="/static/media/dwrz_20220709T004952.mp4" type="video/mp4"> - Your browser does not support video. - </video> - <p> - 晚上9点30分我们在庇护所睡觉。 - 我很累,所以我睡了两个小时。 - 但梅根没睡着。她看到了一些大蜘蛛;庇护所里也又热又湿,还有很多蚊子。 - 当我在晚上11:30起床时,她让我在庇护所搭帐篷。 - 我这样做了,所以剩下的问题只有热度和湿度。 - 那时,听到音乐开始播放。 - 那天晚上是宰牲节,我觉得附近一定有庆祝活动,一直演奏到凌晨四点。 - 总的来说,我们睡得不好。 - </p> - <a href="/static/media/1920/dwrz_20220709T052624.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220709T052624.jpg"> - </a> - <p> - 幸好,第二天天气更好,很凉快。 - 空气更清新了,所以我们可以看到远处的纽约市-玻璃建筑反射着日出。 - </p> - <a href="/static/media/1920/dwrz_20220709T073636.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220709T073636.jpg"> - </a> - <p> - 我们收拾了营地,然后吃早餐;我们吃了梅根做的柠檬罂粟松饼。 - </p> - <a href="/static/media/1920/dwrz_20220709T075543.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220709T075543.jpg"> - </a> - <a href="/static/media/1920/dwrz_20220709T083912.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220709T083912.jpg"> - </a> - <a href="/static/media/1920/dwrz_20220709T084124.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220709T084124.jpg"> - </a> - <p> - 然后,我们远足到手鼓山,然后回到起点。 - 沿着小径,我们吃了很多野蓝莓,谈论生活和我们看到的东西,欣赏另一个方向的景观。 - </p> - <a href="/static/media/1920/dwrz_20220709T085846.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220709T085846.jpg"> - </a> - <a href="/static/media/1920/dwrz_20220709T095929.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220709T095929.jpg"> - </a> - <a href="/static/media/1920/dwrz_20220709T100508.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220709T100508.jpg"> - </a> - <a href="/static/media/1920/dwrz_20220709T100530.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220709T100530.jpg"> - </a> - <p> - 在梅根的车上,我们喝水吃芒果干。回家的路上没有交通拥挤,行程顺利结束。 - </p> - <a href="/static/media/1920/dwrz_20220709T111114.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220709T111114.jpg"> - </a> -</div> diff --git a/cmd/web/site/entry/static/2022-07-08/metadata.json b/cmd/web/site/entry/static/2022-07-08/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "dwrz_20220708T170351.jpg", - "date": "2022-07-08T00:00:00Z", - "published": true, - "title": "远足到西山" -} diff --git a/cmd/web/site/entry/static/2022-10-01/2022-10-01.html b/cmd/web/site/entry/static/2022-10-01/2022-10-01.html @@ -1,33 +0,0 @@ -<!-- src="/static/media/720/maze.png" --> -<div class="wide64"> - <p> - Use the arrow keys to move <span class="purple">Theseus</span>. - <br> - Your goal is to reach the <span class="green">exit</span> -- or to survive - as long as possible. - <br> - Avoid the <span class="red">Minotaur</span>, which is hunting you. - <br> - Press the spacebar to reveal the <span class="blue">solution</span>. - <br> - </p> - <canvas id="maze" height="640" width="640"></canvas> - <p> - <span id="game-over"></span> - </p> -</div> -<script type="module"> - import Maze from '/static/js/minotaur/maze.js'; - import Game from '/static/js/minotaur/game.js'; - - // Set the canvas to be 100% the width of its container. - const canvas = document.getElementById('maze'); - canvas.style.width ='100%'; - - // Create a new maze and game. - const maze = new Maze({ canvas, side: 24 }); - const game = new Game({ document, maze }); - - // Run the game. - game.run(); -</script> diff --git a/cmd/web/site/entry/static/2022-10-01/metadata.json b/cmd/web/site/entry/static/2022-10-01/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "maze.png", - "date": "2022-10-01T00:00:00Z", - "published": true, - "title": "Minotaur" -} diff --git a/cmd/web/site/entry/static/2023-02-01/2023-02-01.html b/cmd/web/site/entry/static/2023-02-01/2023-02-01.html @@ -1,917 +0,0 @@ -<div class="wide64"> - <a href="/static/media/1920/dwrz_20221023T155810_edit.jpg" > - <img class="img-center" - src="/static/media/720/dwrz_20221023T155810_edit.jpg"> - </a> - <p> - I live in a ground floor apartment, and want to keep an eye on the space and - the cats when I am away. A search for security camera systems that offered - timelapse recording, livestream viewing, and instrusion notifications was - unsatisfactory. Most consumer systems were either too expensive, had security - shortcomings, or lacked sufficient user control. As a result, I endend up - assembling a system that meets most of my needs. - </p> - <p> - The solution I've settled on uses three - <a href="https://www.raspberrypi.org/"> Raspberry Pi</a>'s, each acting as a - server connected to a <a href="https://www.webcamerausb.com/">generic fisheye - USB camera</a>. A mix of open-source software and scripts provides a - password protected livestream served over HTTPS, timelapse recording, motion - and object detection, notifications, and remote storage. - </p> - <p> - The cameras come with limitations and vulnerabilties, some shared with - consumer solutions, others unique to a home-brewed setup. But for my needs, - they have worked well, and I have appreciate their modularity, the ability - to repurpose hardware, and full control over the system and the data that it - generates. - </p> - <p> - I was surprised by how quickly I could stand up a system of such disparate - parts — in terms of hardware and software — while writing little code of my - own. Putting these cameras together seemed to confirm some of the - <a href="https://en.wikipedia.org/wiki/Unix_philosophy#Origin">UNIX</a> - principles. It's been possible to connect components with just a few scripts - as glue. - </p> - <p> - Any code referenced on this page should be available here: - <a href="https://code.dwrz.net/vigil/">https://code.dwrz.net/vigil/</a>. - I don't intend to keep code on this page up to date; it should only be used - for example and inspiration. - </p> - <p> - What follows is a rough guide covering the basic components of the system. - It is not intended to be a step-by-step guide, though there is a chance it - might work as one. - </p> - <h2>Hardware</h2> - <a href="/static/media/1920/dwrz_20221022T213439.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20221022T213439.jpg"> - </a> - <p> - I've opted for the following: - <ul> - <li> - <a href="https://www.raspberrypi.com/products/raspberry-pi-400/">Raspberry - Pi 400</a> — easier to source and repurpose for my use cases. I would - have preferred a smaller device with more open hardware and USB ports, - but it was hard to find anything of comparable price. - </li> - <li> - <a - href="https://www.westerndigital.com/products/usb-flash-drives/sandisk-extreme-pro-usb-3-2"> - Sandisk SSD Flash Drive</a> — improves performance and reliability - compared to running the Raspberry Pi off of a MicroSD card. - </li> - <li> - <a - href="https://www.amazon.com/s?me=AVRDPNYMU6GNM&marketplaceID=ATVPDKIKX0DER"> - ELP 3.6mm FHD 180° IR Fisheye Camera</a> — can cover an entire room and - see in the dark. - </li> - <li><a href="https://www.amazon.com/stores/ULIBERMAGNET/page/22395264-96B4-4270-9CA4-518AED93106E">ULIBERMAGNET</a> Tripod Ball Head with Magnetic Base — used to hold and position the cameras.</li> - </ul> - </p> - <h2>Server</h2> - <img class="img-center" src="/static/media/vigil-diagram.svg"> - <p> - I've used the default operating system for Raspberry Pi's, <a href="https://www.raspberrypi.com/software/">Raspberry Pi OS</a>, and this guide assumes that context. I won't cover the operating system installation and setup of an administrative user — documentation is available elsewhere. - </p> - <p> - You should be mindful of the security of the servers themselves. This is a - <a href="https://www.theverge.com/2022/11/30/23486753/anker-eufy-security-camera-cloud-private-encryption-authentication-storage" title="Anker’s Eufy lied to us about the security of its security cameras">problem</a> that even <a href="https://www.bloomberg.com/news/articles/2021-03-09/hackers-expose-tesla-jails-in-breach-of-150-000-security-cams" title="Hackers Breach Thousands of Security Cameras, Exposing Tesla, Jails, Hospitals">commercial</a> <a href="https://www.washingtonpost.com/technology/2019/04/23/how-nest-designed-keep-intruders-out-peoples-homes-effectively-allowed-hackers-get/" title="How Nest, designed to keep intruders out of people’s homes, effectively allowed hackers to get in">offerings</a> <a href="https://www.consumerreports.org/home-security-cameras/wyze-didnt-completely-fix-security-camera-flaws-for-3-years-a3726294358/" title="Wyze Didn't Completely Fix Flaws in Security Cameras for 3 Years">have not handled well</a>. - Your personal circumstances will dictate the balance of features and security. - </p> - - <aside class="bordered br-yellow"> - <p> - When installing the operating system, you'll have to decide whether to - encrypt the filesystem (or a partition). Encryption makes it harder to - extract data if someone is able to gain physical access to the - storage device. However, entering the decryption passwords requires you to - be physically present at the machine. This can be a problem if, e.g., a - transient power outage happens while you are away from the cameras; they - would cease to function until you are on premises again. It's possible to - use something like <code><a href="https://wiki.archlinux.org/title/Dm-crypt/Specialties#Remote_unlocking_(hooks:_netconf,_dropbear,_tinyssh,_ppp)">dropbear</a></code> for boot-time SSH, - to enter the decryption password. But I've opted to use the servers without - encryption, and transfer photos and videos off of the servers as soon as - possible. The downside is that any secrets on the device can be - compromised, requiring remediation in the event of a breach. - </p> - </aside> - <p> - Once you are up and running with Raspberry Pi OS, ensure you are - using the latest software and security updates: - </p> - <pre><code>$ sudo apt-get -y update && sudo apt-get -y dist-upgrade</code></pre> - <p> - Install any dependencies necessary to get work done, e.g.: - </p> - <pre><code>$ sudo apt-get install curl git mg</code></pre> - <p> - Consider enabling <a href="https://wiki.debian.org/UnattendedUpgrades">unattended - security upgrades</a>: - </p> - <pre><code>$ sudo apt-get install unattended-upgrades apt-listchanges</code></pre> - <p> - To receive email reports for unattended upgrades, a - <a href="https://en.wikipedia.org/wiki/Message_transfer_agent"> - Message Transfer Agent - </a> (MTA) and the <code>mailx</code> command are required. This guide - assumes the use of <code><a href="https://marlam.de/msmtp/">msmtp</a></code>: - </p> - <pre><code>$ sudo apt-get install bsd-mailx msmtp msmtp-mta</code></pre> - <p> - Setup the <code>msmtp</code> - <a href="https://marlam.de/msmtp/msmtp.html#Configuration-files">configuration</a> - file for the <code>root</code> user: - </p> - <pre><code>$ sudo mg /root/.msmtprc</code></pre> - <pre><code>defaults -auth on -tls on -tls_trust_file /etc/ssl/certs/ca-certificates.crt -logfile /root/.msmtp.log - -account gmail -host smtp.gmail.com -port 587 -from user@example.com -user user@example.com -password ${PASSWORD} -# Alternatively, a command may be used to retrieve the password: -# passwordeval pass google/gmail/app -# See: https://marlam.de/msmtp/msmtp.html#passwordeval. - -account default : gmail</code></pre> - <p> - This configuration assumes a Gmail or Google Workspace account; you will - need to specify appropriate settings for your own mail provider. If you are - using Gmail or Google Workspaces, you will need to set up an "app password" - for programmatic access. - </p> - <p> - Test that <code>msmtp</code> is working: - </p> - <pre><code>echo "Test" | mailx -s "Test" user@example.com</code></pre> - <p> - Edit the <code>unattended-upgrades</code> configuration to send email - notifications: - </p> - <pre><code>$ sudo mg /etc/apt/apt.conf.d/50unattended-upgrades</code></pre> - <pre><code>Unattended-Upgrade::Mail "user@example.com";</code></pre> - <h2>Networking</h2> - <p> - I assign a static IP address for each of - my servers. Most consumer routers allow for this in their web interface; I - have something like the following in my router's <code>/etc/dhcpd.conf</code>: - <pre><code>host kitchen { - fixed-address 10.0.1.101; - hardware ethernet de:ad:be:ef:8d:8e; -}</code></pre> - </p> - <aside class="bordered br-yellow"> - <p> - If you are using <code>network-manager</code> with a WiFi connection, - you'll need to disable MAC address randomization, or your router / DHCP - server will not be able to consistently match to the device. - </p> - <br> - <pre><code>$ sudo mg /etc/NetworkManager/conf.d/wifi_rand_mac.conf</code></pre> - <br> - <pre><code>[device] -wifi.scan-rand-mac-address=no</code></pre> - </aside> - <p> - Set up a firewall; I block all ports by default, and at most leave three ports - open: one for <code>SSH</code>, one for the camera livestream (e.g., - <code>3000</code>), and optionally one for the web interface (e.g., - <code>8080</code>).</p> - <p> - If you intend to share the livestream over the internet, you'll need a relay - or a port forward from your router. Depending on your network setup, you may - need to enable hairpin NAT or split-horizon DNS to access the servers by their - domain name when on the local network. - </p> - <aside class="bordered br-yellow"> - <p> - I don't recommend allowing for remote access to web interface; and would - advise limiting access to the livestream if possible. <code>motion</code> is - not written in a memory safe language; all security cameras are a - double-edged sword that can compromise your own privacy. - </p> - </aside> - <h2>SSH</h2> - <p>Create an SSH keypair: - </p> - <pre><code>ssh-keygen -t ed25519</code></pre> - <p> - Then, copy the public key to the server: - </p> - <pre><code>ssh-copy-id -i ${SSH_KEY_PATH} username@10.0.1.101 </code></pre> - <p>Add the server to your SSH config: - </p> - <pre><code>Host kitchen - Hostname 10.0.1.101 - IdentityFile ~/.ssh/keys/kitchen</code></pre> - <p> - On the server, disable root login and password authentication; enable public - key authentication. - </p> - <pre><code>$ sudo mg /etc/ssh/sshd_config</code></pre> - <pre><code>PermitRootLogin no -PubkeyAuthentication yes -PasswordAuthentication no</code></pre> - <p> - Restart the SSH daemon: - </p> - <pre><code>$ systemctl restart sshd</code></pre> - <h2>Motion</h2> - <p> - <code><a href="https://motion-project.github.io/">motion</a></code> provides - the core features of the system — multiple cameras, live streams, web - control, motion detection, saving images and movies, timelapse, and event - triggers. Install it: - </p> - <pre><code>$ sudo apt-get install motion</code></pre> - <p> - On Raspberry Pi OS, the installation will create a - <code>motion</code> user, homed at <code>/var/lib/motion/</code>. The - configuration file for <code>motion</code> is located at - <code>/etc/motion/motion.conf</code>; the <code>systemd</code> unit file is - <code>/usr/lib/systemd/system/motion.service</code>. - </p> - <p> - <code>motion</code>'s behavior is controlled via its configuration file. The - <a - href="https://motion-project.github.io/motion_config.html">documentation</a> - covers the settings, and should be reviewed; situation will dictate which - values to use. I discuss configuration below, after setting up the other - components of the system. - </p> - <h2>Object Detection</h2> - <a href="/static/media/1920/dwrz_20221026_1.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20221026_1.jpg"> - </a> - <p> - <code><a href="https://github.com/WongKinYiu/yolov7">yolov7</a></code> - provides the object detection functionality. I use the "tiny" weights for - faster processing on a Raspberry Pi. On a Raspberry Pi 400, I typically see - inferences complete under a second. - </p> - <pre><code>$ sudo apt-get install git pip -$ sudo -u motion bash -$ git clone https://github.com/WongKinYiu/yolov7.git -$ cd yolov7/ -$ pip install -r requirements.txt -$ wget https://github.com/WongKinYiu/yolov7/releases/download/v0.1/yolov7-tiny.pt -$ mkdir -p /tmp/yolov7/</code></pre> - <p> - <a href="https://aws.amazon.com/rekognition/">AWS Rekognition</a> can be used - as an alternative to <code>yolov7</code>. I had better — and cheaper — - results with <code>yolov7</code>. However, if you encounter any issues with - the <code>yolov7</code> installation, AWS offers a convenient fallback. - </p> - <p> - To use Rekognition, you will need to setup an AWS account, install the - <code>aws</code> CLI, and ideally, create an IAM user with permissions - restricted to the Rekognition service. You will also need to - install <code><a href="https://stedolan.github.io/jq/">jq</a></code> to - parse the JSON response from AWS. - </p> - <pre><code>$ sudo apt-get install jq -$ pip3 install --system awscli -$ sudo -u motion bash -$ aws configure</code></pre> - <h2>Notifications</h2> - <a href="/static/media/1920/dwrz_20230124T165950_edit.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20230124T165950_edit.jpg"> - </a> - <p> - I've set things up so that notifications are only sent when two smartphones - are not reachable on the network. The upside is less notifications (though - they'll sometimes come through if the device goes to sleep). If you go this - route, you'll need need to set up static IP addresses for your devices, and - remember to take them with you. - </p> - <p> - I send SMS notifications by emailing my mobile phone number, setting the - recipient to something like <code>1234567890@msg.fi.google.com</code>. That - option may not be available depending on your mobile service provider. A - fallback would be to send notifications to an email address, or to use a - service like <a href="https://www.twilio.com/">Twilio</a> or - <a href="https://aws.amazon.com/sns/">AWS SNS</a>. - </p> - <p> - I include the object-detected camera snapshot in notifications. While it's - not too difficult to write a script or simple program to compose - <a href="https://en.wikipedia.org/wiki/MIME">MIME</a> - emails, it's easier to just install - <code><a href="http://www.mutt.org/">mutt</a></code>. - </p> - <pre><code>$ sudo apt-get install mutt</code></pre> - <p> - Configure <code>msmtp</code> and <code>mutt</code> for the <code>motion</code> - user: - </p> - <pre><code>$ sudo -u motion bash -$ cd ~ -$ mg .msmtprc</code></pre> - <pre><code>defaults -auth on -tls on -tls_trust_file /etc/ssl/certs/ca-certificates.crt -logfile ~/.cache/msmtp.log - -account gmail -host smtp.gmail.com -port 587 -from user@example.com -user user@example.com -password ${PASSWORD} - -account default : gmail</code></pre> - <pre><code>$ chmod 600 .msmtprc -$ mg .muttrc</code></pre> - <pre><code>set sendmail="/usr/bin/msmtp" -set use_from=yes -set from=user@example.com</code></pre> - <h2>Exporting Data</h2> - <p> - I've configured the system to delete snapshots after sending the - corresponding notification. This makes it harder to retrieve data if someone - gains access to the server. However, since I want to be able to review past - snapshots and timelapse footage, I backup the data off the servers. - </p> - <p> - There are several options — <code>rsync</code> or <code>scp</code> files to - a remote server, perhaps one with an encrypted drive. Additionally, or - alternatively, the files can be backed up to the cloud, to a service like - <a href="https://aws.amazon.com/s3/">AWS S3</a> or <a - href="https://www.backblaze.com/b2/cloud-storage.html">Backblaze B2</a>. - </p> - <p> - For <code>b2</code>, I took the following steps: - <ol> - <li>Create an account.</li> - <li>Create a bucket.</li> - <li> - Setup lifecycle rules on the bucket to delete files after a certain - number of days. - </li> - <li>Create an application key.</li> - </ol> - </p> - <p> - On the servers, I install and configure the <code>b2</code> CLI. - </p> - <pre><code>$ sudo apt-get install backblaze-b2 -$ sudo -u motion bash -$ backblaze-b2 authorize-account</code></pre> - <h2>DDNS</h2> - <p> - To make the camera stream available remotely and conveniently — without a - <code>VPN</code>, port forwarding, or dealing with IP addresses — I use - subdomains to reach my cameras. You will need your own domain for similar - functionality. - </p> - <p> - I don't have a static IP from my ISP, so I use Dynamic DNS to keep my - subdomain records updated. A <code>systemd</code> timer regularly runs a - script to update the <code>A</code> and/or <code>AAAA</code> records for the - server's subdomain. - </p> - <p> - The public IP of the server is retrieved with a DNS lookup, using - <code>dig</code>. On Raspberry Pi OS, you'll need to install the - <code>dnsutils</code> package: - </p> - <pre><code>$ sudo apt-get intsall dnsutils</code></pre> - <p> - How you update your records will depend on your registrar. I use - <a href="https://aws.amazon.com/route53/">AWS Route53</a>, and a simple Go - program I wrote called <code> - <a href="https://code.dwrz.net/src/file/cmd/r53/main.go.html"> - r53</a></code>, which wraps around the - <a href="https://github.com/aws/aws-sdk-go-v2/">AWS Go SDK</a> and - <code>dig</code>. - </p> - <p> - A script is probably easier to install. The following isn't as full featured - as <code>r53</code>, but it doesn't require compiling and installing a Go - binary: - </p> - <pre><code>#!/usr/bin/env bash - -readonly HZ="${AWS_HOSTED_ZONE}" -readonly DOMAIN="${HOSTNAME}" - -err() { - echo "[$(date -u +'%Y-%m-%dT%H:%M:%S%:z')]: $*" >&2 -} - -main() { - if ! [[ -x "$(command -v aws)" ]]; then - err "aws cli not installed"; exit 1 - fi - - # Get the IP address. - ip="$(dig -4 +short myip.opendns.com @resolver1.opendns.com)" - if [[ -z "${ip}" ]]; then - err "failed to get ip address"; exit 2 - fi - printf "ip: %s\n" "${ip}" - - # Update the domains. - update='{ - "Comment": "DDNS", - "Changes": [ - { - "Action": "UPSERT", - "ResourceRecordSet": { - "Name": "'"${DOMAIN}"'", - "Type": "A", - "TTL": 300, - "ResourceRecords": [{ "Value": "'"${ip}"'" }] - } - } - ] -}' - - printf "requesting update for %s\n" "${DOMAIN}" - aws route53 change-resource-record-sets \ - --hosted-zone-id "${HZ}" \ - --change-batch "${update}" -} - -main "$@"</code></pre> - <p> - If you are using AWS Route53, you'll need to install and setup the <code>aws</code> CLI for whichever user will run the DDNS service. Again, it's best to create an AWS IAM user with permissions limited to Route53. - </p> - <pre><code>$ pip3 install --system awscli -$ sudo su -# aws configure</pre></code> - <p> - <code>scp</code> the script or the <code>r53</code> binary to the server, - then move it and set appropriate permissions: - </p> - <pre><code>$ scp r53 user@server -$ sudo mv r53 /usr/local/bin/ -$ sudo chmod 755 /usr/local/bin/r53</code></pre> - <p> - Test the command to ensure that it works: - </p> - <pre><code>$ r53 $HOSTNAME</code></pre> - <p> - These are the <code>systemd</code> unit and timer files — install them at - <code>/usr/lib/systemd/system/</code>: - </p> - <pre><code>[Unit] -Description=DDNS -RefuseManualStart=no -RefuseManualStop=yes - -[Service] -Type=oneshot -ExecStart=ddns - -[Install] -WantedBy=timers.target</code></pre> - - <pre><code>[Unit] -Description=DDNS -RefuseManualStart=no -RefuseManualStop=no - -[Timer] -OnBootSec=1min -OnCalendar=*-*-* *:*/5:00 -Persistent=true -RandomizedDelaySec=15 -Unit=ddns.service - -[Install] -WantedBy=default.target</code></pre> - <p> - Then, enable the timer: - </p> - <pre><code>$ systemctl enable --now ddns.timer</code></pre> - <a href="/static/media/1920/dwrz_20221018T191948_edit.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20221018T191948_edit.jpg"> - </a> - <h2>TLS Certificates</h2> - <p> - <code>motion</code> will need TLS certificates to encrypt the livestream, - webcontrol, and authentication for each. We can get free certificates from <a - href="https://letsencrypt.org/">Let's Encrypt</a>, using <code><a - href="https://certbot.eff.org/">certbot</a></code>. The following assumes a - <code> - <a href="https://letsencrypt.org/docs/challenge-types/#dns-01-challenge"> - dns-01 - </a> - </code> challenge with Route53. - </p> - <p> - Install <code>certbot</code> and the <code>python3-certbot-dns-route53</code> - plugin: - </p> - <pre><code>$ sudo apt-get install certbot python3-certbot-dns-route53</code></pre> - <p> - Run <code>certbot</code> to generate certificates: - </p> - <pre><code>$ sudo certbot certonly \ - --agree-tos \ - --email user@example.com \ - --non-interactive \ - --quiet \ - --verbose \ - --dns-route53 \ - -d ${DOMAIN}</code></pre> - <p> - Add the <code>motion</code> user to the <code>ssl-cert</code> group. - - Then, change group ownership for access. - </p> - <pre><code>$ chown -R root:ssl-cert letsencrypt/archive/${DOMAIN} -$ chown -R root:ssl-cert letsencrypt/live/${DOMAIN} -$ chmod 440 letsencrypt/archive/${DOMAIN}/privkey1.pem</code></pre> - <a href="/static/media/1920/dwrz_20230104T104635_edit.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20230104T104635_edit.jpg"> - </a> - <h2>Motion Scripts</h2> - <p> - We're nearly there. Three scripts are used to tie functionality together; - they should be copied over to <code>/var/lib/motion</code> and made - executable by the <code>motion</code> user. I use a - <a href="https://code.dwrz.net/vigil/file/setup.html">script</a> to make - installing the scripts a little easier. - </p> - <p> - An <code>alert</code> script is called when motion is detected; it sends a - notification via email. - </p> - <pre><code>#!/usr/bin/env bash - -# Devices to check -- if populated and up, no notifications are sent. -readonly DEVICES=() -readonly RECIPIENT="${NOTIFICATION_RECIPIENT}" - -check_devices() { - for device in "${DEVICES[@]}"; do - if ping -c 1 -w 1 "${device}" &> "/dev/null"; then - return 0 - fi - done - - return 255 -} - -main() { - # If devices are present, don't notify. - if (( "${#DEVICES[@]}" )); then - if check_devices; then - exit 0 - fi - fi - - echo "${HOSTNAME}: motion detected at $(date '+%Y-%m-%dT%H:%M:%S%:z')." | \ - mutt -s "${HOSTNAME}: Motion Detected" \ - -- "${RECIPIENT}" -} - -main "$@"</code></pre> - <p> - The <code>sync</code> script is used to backup timelapse videos: - </p> - <pre><code>#!/usr/bin/env bash - -readonly BUCKET="${B2_BUCKET}" - -main() { - local filepath="$1" - local name - name="$(basename "${filepath}")" - - backblaze-b2 upload-file \ - --threads 2 \ - "${BUCKET}" \ - "${HOME}/timelapse/${name}" \ - "${HOSTNAME}/timelapse/${name}" - - # Delete outdated files. - # This assumes the timelapse is created on an hourly basis. - rm -f "$1" - find "${HOME}/timelapse/" -mmin +60 -delete -} - -main "$@"</code></pre> - <p> - The <code>notify</code> script sends notifications, and backs up the - snapshots: - </p> - <pre><code>#!/usr/bin/env bash - -# Backblaze B2 Bucket -readonly BUCKET="${B2_BUCKET}" - -# Devices to check -- if populated and up, no notifications are sent. -readonly DEVICES=() - -# COCO Labels -readonly LABEL_PERSON=0 -readonly LABEL_CAT=15 - -# Lockfile to ensure that only one instance of the script is running. -readonly LOCKFILE="/tmp/motion-notify.lock.d" - -# yolov7 working directory. -readonly PROJECT="/tmp/yolov7" - -# Notification recipient. -readonly RECIPIENT="${NOTIFICATION_RECIPIENT}" - -acquire_lock () { - while true; do - if mkdir "${LOCKFILE}"; then - break; - fi - sleep 1 - done -} - -check_devices() { - for device in "${DEVICES[@]}"; do - if ping -c 1 -w 1 "${device}" &> "/dev/null"; then - return 0 - fi - done - - return 255 -} - -detect_objects() { - local filepath="$1" - - python "${HOME}/yolov7/detect.py" \ - --exist-ok \ - --no-trace \ - --save-txt \ - --project "${PROJECT}" \ - --name "motion" \ - --weights "${HOME}/yolov7/yolov7-tiny.pt" \ - --source "${filepath}" -} - -notify() { - local name="$1" - - echo "${HOSTNAME} at $(date '+%Y-%m-%dT%H:%M:%S%:z')" | \ - mutt -a "${PROJECT}/motion/${name}.jpg" \ - -s "${HOSTNAME}: Motion Detected" \ - -- "${RECIPIENT}" -} - -upload() { - local name="$1" - - backblaze-b2 upload-file \ - --threads 2 \ - "${BUCKET}" \ - "${PROJECT}/motion/${name}.jpg" \ - "${HOSTNAME}/photo/${name}.jpg" -} - -delete_outdated() { - local filepath="$1" - - acquire_lock - - rm -f "$1" - rm -f "${PROJECT}/motion/${name}.jpg" - find "${HOME}/photo/" -mmin +5 -delete - find "${PROJECT}/motion/" -iname "*.jpg" -mmin +5 -delete - find "${PROJECT}/motion/labels/" -mmin +5 -delete - - release_lock -} - -release_lock () { - rmdir "${LOCKFILE}" -} - -main() { - local filepath="$1" - local name - name="$(basename "${filepath}" .jpg)" - - # If devices are present, don't notify. - if (( "${#DEVICES[@]}" )); then - if check_devices; then - delete_outdated "${filepath}" "${name}" - exit 0 - fi - fi - - detect_objects "${filepath}" - - # Send a notification if we match any labels. - labels="$(awk '{print $1}' "${PROJECT}/motion/labels/${name}.txt")" - if echo "${labels}" | grep -qw "${LABEL_PERSON}\|${LABEL_CAT}"; then - notify "${name}" - fi - - upload "${name}" - - delete_outdated "${filepath}" "${name}" -} - -main "$@"</code></pre> - <p> - With AWS Rekognition, you'll need to adapt the script. The following will - handle uploading the image to AWS, and check if the labels are actionable: - </p> - <pre><code>labels="$(env aws rekognition detect-labels \ ---min-confidence 90 \ ---image-bytes fileb://"${filepath}" \ -| jq -j '.Labels | .[] | "\n",.Name," ",.Confidence')" - -if grep --quiet "Human\|Cat" <<< "${labels}"; then - echo "${HOSTNAME} at $(date '+%Y-%m-%dT%H:%M:%S%:z')" | \ - mutt -a "${filepath}" \ - -s "${HOSTNAME}: Motion Detected" \ - -- "${RECIPIENT}" -fi</code></pre> - <h2>Motion Config</h2> - <p> - The last step is to configure <code>motion</code> to: - <ul> - <li>Take snapshots on motion detection</li> - <li>Capture a timelapse — one photo per second, one file per hour, synced to the Backblaze B2</li> - <li>Serve webcontrol on port 8080 over HTTPS</li> - <li>Livestream on port 3000 over HTTPS</li> - <li>Notify on motion detection and send object-detected snapshots</li> - <li>Keep minimal amounts of data on the local drive</li> - </ul> - </p> - <pre><code># GENERAL -daemon off -target_dir ${MOTION_DIR} -log_file ${MOTION_LOG_FILE} - -# IMAGE PROCESSING -despeckle_filter EedDl -framerate 24 -text_scale 2 -text_changes on -text_left %$ -text_right %Y-%m-%d-T%H:%M:%S %q - -# MOTION DETECTION -event_gap 1 -threshold 2000 - -# MOVIES -movie_output off -movie_filename /video/%Y-%m-%dT%H:%M:%S-%v - -# PICTURES -picture_output first -picture_filename /photo/%Y-%m-%dT%H-%M-%S_%q - -# TIMELAPSE -timelapse_interval 1 -timelapse_mode hourly -timelapse_fps 60 -timelapse_codec mpg -timelapse_filename /timelapse/%Y-%m-%d-%H-%M-%S - -# WEBCONTROL -webcontrol_auth_method 2 -webcontrol_authentication ${MOTION_USER}:${MOTION_PASSWORD} -webcontrol_port ${PORT_CONTROL} -webcontrol_localhost off -webcontrol_cert ${TLS_CERT} -webcontrol_key ${TLS_KEY} -webcontrol_parms 0 -webcontrol_tls on - -# LIVE STREAM -stream_port ${PORT_STREAM} -stream_localhost off -stream_quality 25 -stream_motion on -stream_maxrate 24 -stream_auth_method 2 -stream_authentication ${MOTION_USER}:${MOTION_PASSWORD} -stream_preview_method 0 -stream_tls on - -# SCRIPTS -on_motion_detected ${MOTION_DIR}/alert -on_movie_end ${MOTION_DIR}/sync %f -on_picture_save ${MOTION_DIR}/notify %f - -# CAMERA -camera_name ${CAMERA_NAME} -videodevice /dev/video0 -height 1080 -width 1920</code></pre> - </p> - <p>Restart <code>motion</code> to use the updated configuration: - </p> - <pre><code>$ systemctl restart motion</code></pre> - <h2>Camera Management</h2> - <p> - I use a simple script to manage the cameras. This examples allows control - over three servers (one of which has two cameras): - </p> - <pre><code>#!/usr/bin/env bash - -readonly WEBCONTROL_PORT="${PORT_CONTROL}" - -readonly cameras=( - "https://${HOST0}:${WEBCONTROL_PORT}/0" -# "https://${HOST0}:${WEBCONTROL_PORT}/1" -# "https://${HOST1}:${WEBCONTROL_PORT}/${CAMERA0}" -# "https://${HOST2}:${WEBCONTROL_PORT}/${CAMERA0}" -) -readonly auth=( - "${MOTION_USER}:${MOTION_PASSWORD}" -# "${USER0}:${PW0}" -# "${USER1}:${PW1}" -# "${USER2}:${PW2}" -) - -err() { - echo "[$(date -u +'%Y-%m-%dT%H:%M:%S%:z')]: $*" >&2 -} - -main() { - local url="detection/status" - - case "$1" in - "capture"|"c") url="detection/snapshot" ;; - "pause"|"p") url="detection/pause" ;; - "start"|"s") url="detection/start" ;; - "status"|"") url="detection/status" ;; - *) err "unrecognized command: $1"; exit 1 - esac - - for i in "${!cameras[@]}"; do - curl --digest --user "${auth[i]}" "${cameras[i]}/${url}" - done -} - -main "$@"</code></pre> - <h2>Next Steps</h2> - <a href="/static/media/1920/dwrz_20220304T224024_edit.jpg" > - <img class="img-center" src="/static/media/720/dwrz_20220304T224024_edit.jpg"> - </a> - <p> - As with all software, this project is a work-in-progress, at times - abandoned, and never completed. There are a few ideas I am exploring as I - continue to prototype the system: - <ul> - <li> - The most urgent task is to make it easier to set up a new server and to - keep configuration consistent across servers. I'm working on minimizing - some of the duplicative work. Another option is to use containers. - </li> - <li> - Use <a href="https://www.openbsd.org/">OpenBSD</a> instead of Raspberry - Pi OS, replacing <code>msmtp</code> with <code>OpenSTMTPD</code>, - <code>acme-client</code> for <code>letsencrypt</code>. The main concern - here is whether wireless and <code>yolov7</code> are sufficiently - performant. - </li> - <li> - Use an in-memory filesystem and forgo the SSD, which might drop ~$30 - from the cost of the system. - </li> - <li> - Use different hardware — wireless or PoE security cameras with an RTSP - stream and motion running on a single server. - </li> - <li> - Improve object detection with <code>yolov7</code> by training the model. - </li> - <li> - Develop my own Go service to replace or wrap around <code>motion</code>, - or replace the bash scripts with Go programs. - </li> - <li> - Add features, like two-way communication. - </li> - <li> - Use <code>yolov7</code> to monitor the camera stream directly, and forgo - the motion detection step. - </li> - </ul> - </p> - <p> - The final thanks must go to the open-source contributors who have made this - approach possible, from the operating system all the way up to the - interpreters. - </p> -</div> diff --git a/cmd/web/site/entry/static/2023-02-01/metadata.json b/cmd/web/site/entry/static/2023-02-01/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "dwrz_20221023T155810_edit.jpg", - "date": "2023-02-01T00:00:00Z", - "published": true, - "title": "Security Cameras" -} diff --git a/cmd/web/site/entry/static/2025-12-01/2025-12-01.html b/cmd/web/site/entry/static/2025-12-01/2025-12-01.html @@ -1,745 +0,0 @@ -<div class="wide64"> - <p> - I first used <a href="https://www.gnu.org/software/emacs/">Emacs</a> as a text editor 20 years ago. For over a decade, I have used it daily — for writing and coding, task and finance management, email, as a calculator, and to interact with local and remote hosts. I continue to discover new functionality and techniques, and was suprised to see how this 50-year old program has adapted to the frontier of technology. - </p> - <p> - This video shows a <a href="https://en.wikipedia.org/wiki/Large_language_model">large language model</a> (LLM), running on my workstation, using Emacs to determine my location, retrieve weather data, and email me the results. By "<a href="https://arxiv.org/abs/2201.11903">thinking</a>", the LLM determines how to chain available tools to achieve the desired result. - </p> -</div> -<video autoplay controls loop muted disablepictureinpicture - class="video video-wide" src="/static/media/llm.mp4" - type="video/mp4"> - Your browser does not support video. -</video> -<div class="wide64"> - <p> - With <a href="https://karthinks.com">karthink</a>'s <a href="https://github.com/karthink/gptel">gptel</a> package and some custom code, Emacs is capable of: - </p> - <ul> - <li>Querying models from hosted providers (<a href="https://www.anthropic.com/">Anthropic</a>, <a href="https://openai.com/">OpenAI</a>, <a href="https://openrouter.ai/">OpenRouter</a>), or local models (<a href="https://github.com/ggml-org/llama.cpp">llama.cpp</a>, <a href="https://ollama.com/">ollama</a>).</li> - <li>Switching between models and configurations with only a few keystrokes.</li> - <li>Saving conversations to the local filesystem, and using them as context for other conversations.</li> - <li>Including files, buffers, and terminals as context for queries.</li> - <li>Searching the web and reading web pages.</li> - <li>Searching, reading, and sending email.</li> - <li>Consulting agendas, projects, and tasks.</li> - <li>Executing Emacs Lisp code and shell commands.</li> - <li>Generating images via the <a href="https://www.comfy.org/">ComfyUI</a> API.</li> - <li>Geolocating the device and checking the current date and time.</li> - <li>Reading <a href="https://en.wikipedia.org/wiki/Man_page">man</a> pages.</li> - <li>Retrieving the user's name and email.</li> - </ul> - <p> - Because LLMs understand and write <a href="https://en.wikipedia.org/wiki/Emacs_Lisp">Emacs Lisp</a> code, they can extend their own capabilities; the improvements are recursive. Below, I note some of the setup required to enable this functionality. - </p> -</div> - -<div class="wide64"> - <h2>Emacs</h2> - <p> - With <code><a href="https://www.gnu.org/software/emacs/manual/html_node/use-package/">use-package</a></code>, <a href="https://melpa.org/">MELPA</a>, and <a href="https://www.passwordstore.org/">pass</a> for password management, a minimal configuration for <code>gptel</code> looks like this: - </p> - <pre><code>(use-package gptel - :commands (gptel gtpel-send gptel-send-region gptel-send-buffer) - :config - (setq gptel-api-key (password-store-get "open-ai/emacs") - gptel-curl--common-args - '("--disable" "--location" "--silent" "--compressed" "-XPOST" "-D-") - gptel-default-mode 'org-mode) - :ensure t)</code></pre> - <p> - This is enough to start querying <a href="https://openai.com/api/">OpenAI's API</a> from Emacs. - </p> - <p> - To use Anthropic's API: - </p> - <pre><code>(gptel-make-anthropic "Anthropic" - :key (password-store-get "anthropic/api/emacs") - :stream t)</code></pre> - <p> - I prefer OpenRouter, to access models across providers: - </p> - <pre><code>(gptel-make-openai "OpenRouter" - :endpoint "/api/v1/chat/completions" - :host "openrouter.ai" - :key (password-store-get "openrouter.ai/keys/emacs") - :models '(anthropic/claude-opus-4.5 - anthropic/claude-sonnet-4.5 - anthropic/claude-3.5-sonnet - cohere/command-a - deepseek/deepseek-r1-0528 - deepseek/deepseek-v3.1-terminus:exacto - google/gemini-3-pro-preview - mistralai/devstral-medium - mistralai/magistral-medium-2506:thinking - moonshotai/kimi-k2-0905:exacto - moonshotai/kimi-k2-thinking - openai/gpt-5.1 - openai/gpt-5.1-codex - openai/gpt-5-pro - perplexity/sonar-deep-research - qwen/qwen3-max - qwen/qwen3-vl-235b-a22b-thinking - qwen/qwen3-coder:exacto - z-ai/glm-4.6:exacto) - :stream t)</code></pre> - <p> - The choice of model depends on the task and its budget. Even where those two parameters are comparable, it is sometimes useful to switch models. One may have a blind spot, where another will have insight. - </p> - <p> - With <code>gptel</code>, it is easy to switch models mid-conversation, or use the output from one model as context for another. For example, I've used <a href="https://www.perplexity.ai/">Perplexity's</a> <a href="https://openrouter.ai/perplexity/sonar-deep-research">Sonar Deep Research</a> to create briefings, then used another LLM to summarize findings or answer specific questions, augmented with web search. - </p> -</div> - -<div class="wide64"> - <h3>Tools</h3> - <p> - Tools augment a model's perception, memory, or capabilities. The <code>gptel-make-tool</code> function allows one to define tools for use by an LLM. - </p> - <p> - When making tools, one can leverage Emacs' existing functionality. For example, the <code>read_url</code> tool uses <code><a href=" https://www.gnu.org/software/emacs/manual/html_node/url/Retrieving-URLs.html">url-retrieve-synchronously</a></code>, while <code>get_user_name</code> and <code>get_user_email</code> read <code><a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/User-Identification.html#index-user_002dfull_002dname">user-full-name</a></code> and <code><a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/User-Identification.html#index-user_002dmail_002daddress">user-mail-address</a></code>. <code>now</code>, used to retrieve the current date and time, uses <code><a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Time-Parsing.html#index-format_002dtime_002dstring">format_time_string</a></code>: - </p> - <pre><code>(gptel-make-tool - :name "now" - :category "time" - :function (lambda () (format-time-string "%Y-%m-%d %H:%M:%S %Z")) - :description "Retrieves the current local date, time, and timezone." - :include t)</code></pre> - <p> - Similarly, if Emacs is <a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Sending-Mail.html">configured to send mail</a>, the tool definition is straightforward: - </p> - <pre><code>(gptel-make-tool - :name "mail_send" - :category "mail" - :confirm t - :description "Send an email with the user's Emacs mail configuration." - :function - (lambda (to subject body) - (with-temp-buffer - (insert "To: " to "\n" - "From: " user-mail-address "\n" - "Subject: " subject "\n\n" - body) - (sendmail-send-it))) - :args - '((:name "to" - :type string - :description "The recipient's email address.") - (:name "subject" - :type string - :description "The subject of the email.") - (:name "body" - :type string - :description "The body of the email text.")))</code></pre> - <p> - For more complex functionality, I prefer writing shell scripts, for several reasons: - <ul> - <li>The tool definitions are simpler. For example, my <code>qwen-image</code> script includes a large <code>JSON</code> object for the ComfyUI flow. I prefer to leave it outside my Emacs configuration.</li> - <li>Tools are accessible to LLMs that may not be running in the Emacs environment (agents, one-off scripts).</li> - <li>Fluency. LLMs seem better at writing bash (or Python, or Go) than Emacs Lisp, so it easier to lean on this inherent expertise in developing the tools themselves.</li> - </ul> - </p> - <img class="img-center" src="/static/media/drawing-hands.jpg"> - <div class="caption"> - <p>M.C. Escher, <i>Drawing Hands</i> (1948)</p> - </div> -</div> - -<div class="wide64"> - <h4>Web Search</h4> - <p> - For example, for web search, I initially used the tool described in the <code>gptel</code> <a href="https://github.com/karthink/gptel/wiki/Tools-collection">wiki</a>: - </p> - <pre><code>(defvar brave-search-api-key (password-store-get "search.brave.com/api/emacs") - "API key for accessing the Brave Search API.") - -(defun brave-search-query (query) - "Perform a web search using the Brave Search API with the given QUERY." - (let ((url-request-method "GET") - (url-request-extra-headers - `(("X-Subscription-Token" . ,brave-search-api-key))) - (url (format "https://api.search.brave.com/res/v1/web/search?q=%s" - (url-encode-url query)))) - (with-current-buffer (url-retrieve-synchronously url) - (goto-char (point-min)) - (when (re-search-forward "^$" nil 'move) - (let ((json-object-type 'hash-table)) - (json-parse-string - (buffer-substring-no-properties (point) (point-max)))))))) - -(gptel-make-tool - :name "brave_search" - :category "web" - :function #'brave-search-query - :description "Perform a web search using the Brave Search API" - :args (list '(:name "query" - :type string - :description "The search query string")))</code></pre> - <p> - However, there are times I want to inspect the search results. I use this script: - </p> - <pre><code>#!/usr/bin/env bash - -set -euo pipefail - -API_URL="https://api.search.brave.com/res/v1/web/search" - -check_deps() { - for cmd in curl jq pass; do - command -v "${cmd}" >/dev/null || { - echo "missing: ${cmd}" >&2 - exit 1 - } - done -} - -perform_search() { - local query="${1}" - local res - - res=$(curl -s -G \ - -H "X-Subscription-Token: $(pass "search.brave.com/api/emacs")" \ - -H "Accept: application/json" \ - --data-urlencode "q=${query}" \ - "${API_URL}") - if echo "${res}" | jq -e . >/dev/null 2>&1; then - echo "${res}" - else - echo "error: failed to retrieve valid JSON res: ${res}" >&2 - exit 1 - fi -} - -main() { - check_deps - - if [ $# -eq 0 ]; then - echo "Usage: ${0} <query>" >&2 - exit 1 - fi - - perform_search "${*}" -} - -main "${@}"</code></pre> - <p> - Which can be called manually from a shell: <code>brave-search 'quine definition' | jq -C | less</code>. - </p> - <p> - The tool definition condenses to: - </p> - <pre><code>(gptel-make-tool - :name "brave_search" - :category "web" - :function - (lambda (query) - (shell-command-to-string - (format "brave-search %s" - (shell-quote-argument query)))) - :description "Perform a web search using the Brave Search API" - :args - (list '(:name "query" - :type string - :description "The search query string")))</code></pre> -</div> -<div class="wide64"> - <h4>Context</h4> - <p> - One limitation that I have run into with tools is context overflow — when retrieved data exceeds an LLM's context window. - </p> - <p> - For example, this tool lets an LLM read <code>man</code> pages, helping it correctly recall command flags: - </p> - <pre><code>(gptel-make-tool - :name "man" - :category "documentation" - :function - (lambda (page_name) - (shell-command-to-string - (concat "man --pager cat" page_name))) - :description "Read a Unix manual page." - :args - '((:name "page_name" - :type string - :description - "The name of the man page to read. Can optionally include a section number, for example: '2 read' or 'cat(1)'.")))</code></pre> - - <p> - It broke when calling the <a href="https://www.gnu.org/software/units/">GNU units</a> <code>man</code> page, which exceeds 40,000 tokens on my system. This was unfortunate, since some coversions, like temperature, are unintuitive: - </p> - - <pre><code>units 'tempC(100)' tempF</code></pre> - <p> - With <code>gptel</code>, one fallback is Emacs' built in <code>man</code> functionality. The appropriate region can be selected with <code>-r</code> in the transient menu. In some cases, this is faster than a tool call. - </p> -</div> -<video autoplay controls loop muted disablepictureinpicture - class="video" src="/static/media/llm-temp.mp4" - type="video/mp4"> - Your browser does not support video. -</video> -<div class="wide64"> - <p> - I ran into a similar problem with the <code>read_url</code> tool (also found on <a href="https://github.com/karthink/gptel/wiki/Tools-collection">gptel wiki</a>). It can break if the response is larger than the context window. - </p> - <pre><code>(gptel-make-tool - :name "read_url" - :category "web" - :function - (lambda (url) - (with-current-buffer - (url-retrieve-synchronously url) - (goto-char (point-min)) (forward-paragraph) - (let ((dom (libxml-parse-html-region - (point) (point-max)))) - (run-at-time 0 nil #'kill-buffer - (current-buffer)) - (with-temp-buffer - (shr-insert-document dom) - (buffer-substring-no-properties - (point-min) - (point-max)))))) - :description "Fetch and read the contents of a URL" - :args (list '(:name "url" - :type string - :description "The URL to read")))</code></pre> - <p> - When I have run into this problem, the issue was bloated functional content — JavaScript code CSS. If the content is not dynamically generated, one call fallback to Emacs' web browser, <code><a href="https://www.gnu.org/software/emacs/manual/html_mono/eww.html">eww</a></code>. The buffer or selected regions can be added as context. A more sophisticated tool could help in these cases. Long term, I hope that LLMs will steer the web back towards readability, either by acting as an aggregator and filter, or as evolutionary pressure in favor of static content. - </p> -</div> - -<div class="wide64"> - <h4>Security</h4> - <p> - The <code><a href="https://github.com/karthink/gptel/wiki/Tools-collection#run_command">run_command</a></code> tool, also found in the <code>gptel</code> tool collection, enables shell command execution, and requires care. A compromised model could issue malicious commands, or a poorly prepared command could have unintended consequences. <code>gptel</code>'s <code>:confirm</code> key can be used to inspect and approve tool calls. - </p> - - <pre><code>(gptel-make-tool - :name "run_command" - :category "command" - :confirm t - :function - (lambda (command) - (with-temp-message - (format "Executing command: =%s=" command) - (shell-command-to-string command))) - :description - "Execute a shell command; returns the output as a string." - :args - '((:name "command" - :type string - :description "The complete shell command to execute.")))</code></pre> - - <p> - Inspection limits the LLM's ability to operate asynchronously, without human intervention. There are a few solutions to this problem, the easiest being to offer tools with more limited scope. - </p> -</div> - -<video autoplay controls loop muted disablepictureinpicture - class="video" src="/static/media/llm-inspect.mp4" - type="video/mp4"> - Your browser does not support video. -</video> - -<div class="wide64"> - <h3>Presets</h3> - <p> - With <code>gptel</code>'s transient menu, only a few keystrokes are need to add, edit, or remove context, switch the model one wants to query, change the input and output, or edit the system message. Presets accelerate switching between settings, and are defined with <code>gptel-make-preset</code>. - </p> - <p> - For example, with <a href="https://huggingface.co/openai/gpt-oss-120b">GPT-OSS 120B</a> (one of OpenAI's <a href="https://openai.com/open-models/">open weights</a> models), a system prompt is necessary to minimize the use of tables and excessive text styling. A preset can load the appropriate settings: - </p> - <pre><code>(gptel-make-preset 'assistant/gpt - :description "GPT-OSS general assistant." - :backend "llama.cpp" - :model 'gpt - :include-reasoning nil - :system - "You are a large language model queried from Emacs. Your conversation with the user occurs in an org-mode buffer. - -- Use org-mode syntax only (no Markdown). -- Use tables ONLY for tabular data with few columns and rows. -- Avoid extended text in table cells. If cells need paragraphs, use a list instead. -- Default to plain paragraphs and simple lists. -- Minimize styling. Use *bold* or /italic/ only where emphasis is essential. Use ~code~ for technical terms. -- If citing facts or resources, output references as org-mode links. -- Use code blocks for calculations or code examples.")</code></pre> - <p> - From the transient menu, this preset can be selected with two keystrokes: <code>@</code> and then <code>a</code>. Alternatively, the preset can be used in the last prompt, like so: <code>@assistant/gpt When is the solstice this year?</code> - </p> -</div> - -<div class="wide64"> - <h4>Memory</h4> - <p> - Presets can be used to implement read-only memory for an LLM. This preset uses <a href="https://huggingface.co/Qwen/Qwen3-VL-30B-A3B-Thinking">Qwen3 VL 30B-A3B</a> with a <code>memory.org</code> file automatically included in the context: - </p> - - <pre><code>(gptel-make-preset 'assistant/qwen - :description "Qwen Emacs assistant." - :backend "llama.cpp" - :model 'qwen3_vl_30b-a3b - :context '("~/memory.org"))</code></pre> - - <p> - The file can include any information that should always be included as context. One could also grant LLMs the ability to append to <code>memory.org</code>, though I am skeptical that they would do so judiciously. - </p> -</div> - -<div class="wide64"> - <h2>Local LLMs</h2> - <p> - Running LLMs on one's own devices offers some advantages over third-party providers: - <ul> - <li>Redundancy: they work offline, even if providers are experiencing an outage.</li> - <li>Privacy: queries and data remain on the device.</li> - <li>Control: You know exactly which model is running, with what settings, at what quantization.</li> - </ul> - </p> - <p> - The main trade-off is intelligence, though for many purposes, the gap is closing fast. Local models excel at summarizing data, language translation, image and PDF extraction, and simple research tasks. I rely on hosted models primarily for complex coding tasks, or when a larger effective context is required. - </p> - <h3>llama.cpp</h3> - <p> - <a href="https://github.com/ggml-org/llama.cpp">llama.cpp</a> makes it easy to run models locally: - </p> - <pre><code>git clone https://github.com/ggml-org/llama.cpp.git - -cd llama.cpp - -cmake -B build - -cmake --build build --config Release - -mv build/bin/llama-server ~/.local/bin/ # Or elsewhere in PATH. - -llama-server -hf unsloth/Qwen3-4B-GGUF:q8_0</code></pre> - <p> - This will build <code>llama.cpp</code> with support for CPU based inference, move <code>llama-server</code> into <code>~/.local/bin/</code>, and then download and run <a href="https://unsloth.ai/">Unsloth</a>'s <code>Q8</code> quantization of the <a href="https://huggingface.co/Qwen/Qwen3-4B">Qwen3 4B</a>. The <code>llama.cpp</code> <a href="https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md"> documentation</a> explains how to build for GPUs and other hardware — not much more work than the default build. - </p> - <p><code>llama-server</code> offers a web interface, available at port 8080 by default.</p> -</div> - -<video autoplay controls loop muted disablepictureinpicture - class="video" src="/static/media/llm-ls.mp4" - type="video/mp4"> - Your browser does not support video. -</video> - -<div class="wide64"> - <h3>Weights</h3> - <p> - Part of the art of using LLMs is selecting an appropriate model. Some factors to consider are available hardware, intended use (task, language), and desired pricing (input and output costs). Some models offer specialized capabilities — <a href="https://ai.google.dev/gemma/docs/core">Gemma3</a> and <a href=""https://github.com/QwenLM/Qwen3-VL">Qwen3-VL</a> offer multimodal input, <a href="https://deepmind.google/models/gemma/medgemma/">Medgemma</a> specializes in medical knowledge, and <a href=https://mistral.ai/">Mistral</a>'s <a href="https://mistral.ai/news/devstral">Devstral</a> focuses on agentic use. - </p> - <p> - For local use, hardware tends to be the main limiter. One has to fit the model into available memory, and consider the acceptable performance for one's use case. A rough guideline is to use the smallest model or quantization for the required task. Or, from the opposite direction, to look for the largest model that can fit into available memory. The rule of thumb is that a <code>Q8_0</code> quantization uses about as much memory as there are parameters, so an 8 billion parameter model will use about 8 GB of RAM or VRAM. A <code>Q4_0</code> quant would use half that — 4 GB — while at 16-bit, 16 GB. - </p> - <p> - My workstation, laptop, and mobile (<code>llama.cpp</code> can be used from <code><a href="https://termux.dev/en/">termux</a></code>) all run different classes of weights. On my mobile device, I have about 12GB of RAM, but background utilization is already around 8GB. So, when necessary, I use 4B models at <code>Q8_0</code> or less: Gemma3, Qwen3-VL, and Medgemma. If a laptop has 16GB of RAM with 2GB in use, 8B models might run well enough. The workstation, which has a GPU, can run larger models, with longer context, faster. There are other tricks one can use — <a href="https://huggingface.co/docs/text-generation-inference/en/conceptual/flash_attention">flash attention</a>, <a href="https://research.google/blog/looking-back-at-speculative-decoding/">speculative decoding</a>, MoE offloading — to optimize performance across different hardware configurations. - </p> -</div> - -<div class="wide64"> - <h3>llama-swap</h3> - <p> - One current limitation of <code>llama.cpp</code> is that unless you load multiple models at once, switching models requires manually starting a new instance of <code>llama-server</code>. To swap models on demand, <code><a href="https://github.com/mostlygeek/llama-swap">llama-swap</a></code> can be used. - </p> - <p> - <code>llama-swap</code> uses a <code>YAML</code> configuration file, which is <a href="https://github.com/mostlygeek/llama-swap/wiki/Configuration">well documented</a>. I use something like the following: - </p> - <pre><code>logLevel: debug - -macros: - "models": "/home/llama-swap/models" - -models: - gemma3: - cmd: | - llama-server - --ctx-size 0 - --gpu-layers 888 - --jinja - --min-p 0.0 - --model ${models}/gemma-3-27b-it-ud-q8_k_xl.gguf - --mmproj ${models}/mmproj-gemma3-27b-bf16.gguf - --port ${PORT} - --repeat-penalty 1.0 - --temp 1.0 - --top-k 64 - --top-p 0.95 - ttl: 900 - name: "gemma3_27b" - gpt: - cmd: | - llama-server - --chat-template-kwargs '{"reasoning_effort": "high"}' - --ctx-size 0 - --gpu-layers 888 - --jinja - --model ${models}/gpt-oss-120b-f16.gguf - --port ${PORT} - --temp 1.0 - --top-k 0 - --top-p 1.0 - ttl: 900 - name: "gpt-oss_120b" - qwen3_vl_30b-a3b: - cmd: | - llama-server - --ctx-size 131072 - --gpu-layers 888 - --jinja - --min-p 0 - --model ${models}/qwen3-vl-30b-a3b-thinking-ud-q8_k_xl.gguf - --mmproj ${models}/mmproj-qwen3-vl-30ba3b-bf16.gguf - --port ${PORT} - --temp 0.6 - --top-k 20 - --top-p 0.95 - ttl: 900 - name: "qwen3_vl_30b-a3b-thinking"</code></pre> -</div> -<div class="wide64"> - <h3>nginx</h3> - <p> - Since my workstation has a GPU and can be accessed on the local network or via <a href="https://www.wireguard.com/">WireGuard</a> from other devices, I use <code><a href="https://nginx.org/">nginx</a></code> as a reverse proxy in front of <code>llama-swap</code>, with certificates generated by <code><a href="https://certbot.eff.org/">certbot</a></code>. For streaming LLM responses, <code>proxy_buffering off;</code> and <code>proxy_cache off;</code> are essential settings. - </p> - - <pre><code>user http; -worker_processes 1; -worker_cpu_affinity auto; - -events { - worker_connections 1024; -} - -http { - charset utf-8; - sendfile on; - tcp_nopush on; - tcp_nodelay on; - server_tokens off; - types_hash_max_size 4096; - client_max_body_size 32M; - - # MIME - include mime.types; - default_type application/octet-stream; - - # logging - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log warn; - - include /etc/nginx/conf.d/*.conf; -}</code></pre> - - <p>Then, for <code>/etc/nginx/conf.d/llama-swap.conf</code>:</p> - - <pre><code>server { - listen 80; - server_name llm.dwrz.net; - return 301 https://$server_name$request_uri; -} - -server { - listen 443 ssl; - http2 on; - server_name llm.dwrz.net; - - ssl_certificate /etc/letsencrypt/live/llm.dwrz.net/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/llm.dwrz.net/privkey.pem; - - location / { - proxy_buffering off; - proxy_cache off; - proxy_pass http://localhost:11434; - proxy_read_timeout 3600s; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -}</code></pre> -</div> -<div class="wide64"> - <h3>Emacs Configuration</h3> - - <p> - <code>llama-server</code> offers an <a href="https://platform.openai.com/docs/api-reference/introduction">OpenAI API</a> compatible API. <code>gptel</code> can be configured to utilize local models with something like the following: - </p> - - <pre><code>(gptel-make-openai "llama.cpp" - :stream t - :protocol "http" - :host "localhost" - :models - '((gemma3 - :capabilities (media tool json url) - :mime-types ("image/jpeg" - "image/png" - "image/gif" - "image/webp")) - gpt - (medgemma_27b - :capabilities (media tool json url) - :mime-types ("image/jpeg" - "image/png" - "image/gif" - "image/webp")) - (qwen3_vl_30b-a3b - :capabilities (media tool json url) - :mime-types ("image/jpeg" - "image/png" - "image/gif" - "image/webp")) - (qwen3_vl_32b - :capabilities (media tool json url) - :mime-types ("image/jpeg" - "image/png" - "image/gif" - "image/webp"))))</code></pre> -</div> -<div class="wide64"> - <h2>Techniques</h2> - <p> - Having covered the setup and configuration, here are some practical ways I use Emacs with LLMs, demonstrated with examples: - </p> -</div> -<div class="wide64"> - <h3>Simple Q&A</h3> - <p> - With the <code>gptel</code> transient menu, press <code>m</code> to prompt from the minibuffer, and <code>e</code> to output the answer to the echo area, then <code>Enter</code> to input the prompt. - </p> -</div> - -<video autoplay controls loop muted disablepictureinpicture - class="video" src="/static/media/llm-qa.mp4" - type="video/mp4"> - Your browser does not support video. -</video> - -<div class="wide64"> - <h3>Brief Conversations</h3> - - <p> - For brief multi-turn conversations that require no persistence, <code>gptel</code> can be used in the <code>*scratch*</code> buffer. Context can be added via the transient menu, <code>-b</code>, <code>-f</code>, or <code>-r</code> as necessary. The conversation is not persisted unless the buffer is saved. - </p> - - <h3>Image-to-Text</h3> - <p> - With multimodal LLMs like Gemma3 and Qwen3-VL, one can extract text and tables from images. - </p> -</div> - -<video autoplay controls loop muted disablepictureinpicture - class="video" src="/static/media/llm-itt.mp4" - type="video/mp4"> - Your browser does not support video. -</video> - -<div class="wide64"> - <h3>Text-to-Image</h3> - <p> - My primary use case is to revisit themes from some of my dreams. Here, a local LLM retrieves a URL, reads its contents, and then generates an image with ComfyUI: - </p> -</div> -<video autoplay controls loop muted disablepictureinpicture - class="video" src="/static/media/llm-image.mp4" - type="video/mp4"> - Your browser does not support video. -</video> - -<div class="wide64"> - <p> - The result: - <img class="img-center" src="/static/media/comfy-ui-dream.png"> - </p> -</div> - -<div class="wide64"> - <h3>Research</h3> - <p> - If I know I will need to reference a topic later, I usually start out with an <code><a href="https://orgmode.org/">org-mode</a></code> file. In this case, I tend to use links to construct context, something like this: - - <img class="img-center" src="/static/media/llm-links.png"> - </p> -</div> - -<div class="wide64"> - <h3>Rewrites</h3> - <p> - Although I don't use it very often, <code>gptel</code> comes with rewrite functionality, activated when the transient menu is called on a seleted region. It can be used on both text and code, and the output can be <code>diff</code>ed, iterated on, accepted, or rejected. Additionally, it can serve as a kind of autocomplete, by having an LLM implement the skeleton of a function or code block. - </p> -</div> - -<div class="wide64"> - <h3>Translation</h3> - <p> - For small or unimportant text, Google Translate via the command-line with <code><a href="https://github.com/soimort/translate-shell">translate-shell</a></code> works well enough. Otherwise, I find the translation output from local LLMs is typically more sensitive to context. - </p> -</div> - -<video autoplay controls loop muted disablepictureinpicture - class="video" src="/static/media/llm-translate.mp4" - type="video/mp4"> - Your browser does not support video. -</video> - -<div class="wide64"> - <h3>Code</h3> - <p> - My experience using LLMs for code has been mixed. For scripts and small programs, iterating in a single conversation works well. However, with larger codebases, I have not found that LLMs can contribute meaningfully, reliably. This used to be an area of relative strength for hosted models, but I surmise aggressive quantization has begun to reduce their effectiveness. - </p> - - <p> - So far, I have had limited success with agents. My experience has been that they burn through tokens to understand context, but still manage to miss important nuance. This experience has made me hesitant to add tool support for file operations. I am actively exploring some techniques on this front. - </p> - - <p> - For now, I have come to distrust the initial output from any model. Instead, I provide context through <code>org-mode</code> links in project-specific files. I have LLM(s) walk through potential changes, which I review and implement by hand. Generally, this approach saves time, but often, I still work faster on my own. - </p> -</div> - -<div class="wide64"> - <h2>Reflections</h2> - <blockquote> - <p> - <i> - The question of whether a computer can think is no more interesting than - the question of whether a submarine can swim. - </i> - </p> - - <p> - Edsger Dijkstra - </p> - </blockquote> - - <p> - Despite encountering frustrations with LLM use, it is hard to shake - the feeling of experiencing a leap in capability. There is something - magical to the technology, especially when run locally — the coil whine of - the GPU evoking the spirit of Rodin's - <a href="https://en.wikipedia.org/wiki/The_Thinker"><i>Thinker</i></a>. - Learning <a href="https://www.3blue1brown.com/topics/neural-networks">how - LLMs work</a> has offered<a href="https://arxiv.org/abs/2007.09560"> - another lens</a> through which to view the world. - </p> - - <p> - My hope is that time will distribute and democratize the technology, in terms of hardware (for local use) and software (system integration). For most users, the barrier to entry for Emacs is high. Other frontends could unlock comparable power and flexibility with support for: - <ul> - <li>The ability to assist the user in developing custom tools</li> - <li>Notebooks featuring executable code blocks</li> - <li>Links for local and remote content, including other conversations</li> - <li>Switching models and providers at any point</li> - <li>Mail and task integration</li> - <li>Offline operation with local models</li> - <li>Remote access — Emacs can be accessed via <code><a href="https://www.openssh.org/">SSH</a></code>, <code>gptel</code> files via <code><a href="https://www.gnu.org/software/tramp/">TRAMP</a></code></li> - </ul> - </p> - - <p> - There are many topics of concern and discussion around LLMs. From my work with them so far, I am more anxious about some than others. Local inference alone reveals how much energy these models can require. On the other hand, the limitations of the technology leave me extremely skeptical of imminent superintelligence. But what we have now, limitations included, is useful — and has potential. - </p> -</div> diff --git a/cmd/web/site/entry/static/2025-12-01/metadata.json b/cmd/web/site/entry/static/2025-12-01/metadata.json @@ -1,6 +0,0 @@ -{ - "cover": "drawing-hands.jpg", - "date": "2025-12-01T00:00:00Z", - "published": true, - "title": "Recursive Intelligence: Using LLMs with Emacs" -} diff --git a/cmd/web/site/error.go b/cmd/web/site/error.go @@ -1,58 +0,0 @@ -package site - -import ( - "bytes" - "fmt" - "net/http" - "runtime/debug" - - "code.dwrz.net/src/cmd/web/site/page" -) - -const ( - defaultErrorMessage = "Sorry, something went wrong." -) - -type Error struct { - Code int - Error error - Message string -} - -func (s *Site) error(w http.ResponseWriter, r *http.Request, e *Error) { - var ( - id = r.Context().Value("id").(string) - trace = string(debug.Stack()) - ) - - // Log with the line number of the caller. - s.log.Error.Output( - 2, - fmt.Sprintf("%s → %d error: %v", id, e.Code, e.Error), - ) - if s.debug { - s.log.Error.Output(2, fmt.Sprintf( - "%s → TRACE:\n%s\n", id, trace, - )) - } - - var p = &bytes.Buffer{} - if err := s.tmpl.ExecuteTemplate( - p, "base", &page.Error{ - Debug: s.debug, - Message: e.Message, - RequestId: id, - Text: e.Error.Error(), - Trace: trace, - }, - ); err != nil { - s.log.Error.Printf("%s → failed to render page: %v", id, err) - - w.WriteHeader(e.Code) - w.Write([]byte(defaultErrorMessage)) - return - } - - w.WriteHeader(e.Code) - w.Write(p.Bytes()) -} diff --git a/cmd/web/site/new.go b/cmd/web/site/new.go @@ -1,107 +0,0 @@ -package site - -import ( - "bytes" - "fmt" - - "code.dwrz.net/src/cmd/web/site/entry" - "code.dwrz.net/src/cmd/web/site/page" - "code.dwrz.net/src/cmd/web/site/templates" - "code.dwrz.net/src/pkg/log" -) - -type Params struct { - Debug bool - Log *log.Logger -} - -func (p *Params) Validate() error { - if p.Log == nil { - return fmt.Errorf("missing logger") - } - - return nil -} - -func New(p Params) (*Site, error) { - if err := p.Validate(); err != nil { - return nil, fmt.Errorf("invalid params: %v", err) - } - - var site = &Site{ - debug: p.Debug, - files: static, - log: p.Log, - pages: map[string]*bytes.Buffer{}, - } - - // Load the templates. - tmpl, err := templates.Parse() - if err != nil { - return nil, fmt.Errorf("failed to load templates: %v", err) - } - site.tmpl = tmpl - - // Load the entries. - entries, err := entry.Load(entry.LoadParams{Log: p.Log}) - if err != nil { - return nil, fmt.Errorf("failed to load entries: %v", err) - } - - // Render contact page. - var contact = &bytes.Buffer{} - if err := site.tmpl.ExecuteTemplate( - contact, "base", &page.Contact{}, - ); err != nil { - return nil, fmt.Errorf("failed to exec template: %v", err) - } - site.pages["/contact/"] = contact - - // Render cv page. - var cv = &bytes.Buffer{} - if err := site.tmpl.ExecuteTemplate( - cv, "base", &page.CV{}, - ); err != nil { - return nil, fmt.Errorf("failed to exec template: %v", err) - } - site.pages["/cv/"] = cv - - // Render the home page. - var home = &bytes.Buffer{} - if err := site.tmpl.ExecuteTemplate( - home, "base", &page.Home{}, - ); err != nil { - return nil, fmt.Errorf("failed to exec template: %v", err) - } - site.pages["/"] = home - - // Render the timeline page. - var timeline = &bytes.Buffer{} - if err := site.tmpl.ExecuteTemplate( - timeline, "base", &page.Timeline{ - Years: entry.SortYear(entries), - }, - ); err != nil { - return nil, fmt.Errorf("failed to exec template: %v", err) - } - site.pages["/timeline/"] = timeline - - // Render the entry pages. - for _, e := range entries { - var p = &bytes.Buffer{} - if err := site.tmpl.ExecuteTemplate( - p, "base", &page.Entry{Entry: e}, - ); err != nil { - return nil, fmt.Errorf( - "failed to exec template: %v", err, - ) - } - - path := fmt.Sprintf( - "/timeline/%s/", e.Date.Format(entry.DateFormat), - ) - site.pages[path] = p - } - - return site, nil -} diff --git a/cmd/web/site/page.go b/cmd/web/site/page.go @@ -1,27 +0,0 @@ -package site - -import ( - "fmt" - "net/http" -) - -func (s *Site) page(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - if page := s.pages[r.URL.Path]; page != nil { - w.WriteHeader(http.StatusOK) - w.Write(page.Bytes()) - return - } - - s.error(w, r, &Error{ - Code: http.StatusNotFound, - Error: fmt.Errorf("no page for %s", r.URL.Path), - Message: fmt.Sprintf( - "Sorry, but %s does not exist.", r.URL.Path, - ), - }) -} diff --git a/cmd/web/site/page/page.go b/cmd/web/site/page/page.go @@ -1,51 +0,0 @@ -package page - -import ( - "code.dwrz.net/src/cmd/web/site/entry" -) - -type Contact struct{} - -func (p *Contact) View() string { - return "contact" -} - -type CV struct{} - -func (p *CV) View() string { - return "cv" -} - -type Entry struct { - Entry *entry.Entry -} - -func (p *Entry) View() string { - return "entry" -} - -type Error struct { - Debug bool - Message string - RequestId string - Text string - Trace string -} - -func (p *Error) View() string { - return "error" -} - -type Home struct{} - -func (p *Home) View() string { - return "home" -} - -type Timeline struct { - Years []entry.Year -} - -func (p *Timeline) View() string { - return "timeline" -} diff --git a/cmd/web/site/site.go b/cmd/web/site/site.go @@ -1,86 +0,0 @@ -package site - -import ( - "bytes" - "context" - "embed" - "fmt" - "html/template" - "net/http" - "path" - "strings" - "time" - - "code.dwrz.net/src/pkg/log" - "code.dwrz.net/src/pkg/randstr" -) - -//go:embed all:static/* -var static embed.FS - -type Site struct { - debug bool - files embed.FS - log *log.Logger - pages map[string]*bytes.Buffer - tmpl *template.Template -} - -func (s *Site) ServeHTTP(w http.ResponseWriter, r *http.Request) { - now := time.Now() - - // Recover and handle panics. - defer func() { - if err := recover(); err != nil { - w.Header().Set("Connection", "close") - - s.error(w, r, &Error{ - Code: http.StatusInternalServerError, - Error: fmt.Errorf("%s", err), - Message: "Sorry, something went wrong.", - }) - } - }() - - // Generate an id to track the request. - // Update the request context to store the id. - id := randstr.Charset(randstr.Numeric, 8) - r = r.WithContext(context.WithValue(r.Context(), "id", id)) - - // Log the request. - var origin = r.RemoteAddr - if ip := r.Header.Get("X-Forwarded-For"); ip != "" { - origin = ip - } - s.log.Debug.Printf( - "%s → %s %s %s %s", - id, origin, - r.Proto, r.Method, r.URL.RequestURI(), - ) - - // Pass the request on to the multiplexer. - base, _ := shiftpath(r.URL.Path) - switch base { - case "static": - s.static(w, r) - case "status": - s.status(w, r) - default: - s.page(w, r) - } - - s.log.Debug.Printf( - "%s → completed in %v", id, time.Since(now), - ) -} - -func shiftpath(p string) (base, rest string) { - p = path.Clean("/" + p) - - i := strings.Index(p[1:], "/") + 1 - if i <= 0 { - return p[1:], "/" - } - - return p[1:i], p[i:] -} diff --git a/cmd/web/site/static.go b/cmd/web/site/static.go @@ -1,41 +0,0 @@ -package site - -import ( - "fmt" - "net/http" - "path/filepath" -) - -func (s *Site) static(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - f, err := s.files.ReadFile(r.URL.Path[1:]) - if err != nil { - s.error(w, r, &Error{ - Code: http.StatusNotFound, - Error: fmt.Errorf("failed to open: %v", err), - Message: fmt.Sprintf( - "Failed to retrieve file: %s", - r.URL.Path, - ), - }) - return - } - - switch filepath.Ext(r.URL.Path) { - case ".css": - w.Header().Add("Content-Type", "text/css; charset=utf-8") - case ".js": - w.Header().Add("Content-Type", "text/javascript; charset=utf-8") - case ".svg": - w.Header().Add("Content-Type", "image/svg+xml; charset=utf-8") - default: - w.Header().Add("Content-Type", http.DetectContentType(f)) - } - - w.WriteHeader(http.StatusOK) - w.Write(f) -} diff --git a/cmd/web/site/static/css/dwrz.css b/cmd/web/site/static/css/dwrz.css @@ -1,587 +0,0 @@ -html { - background-color: #efefef; - color: #1d1f21; - font-family: serif; - margin: 1em; -} - -body { - margin: 0em auto 0em auto; -} - -main { - background-color: #ffffff; - margin: 1em auto 1em auto; -} - -header { - background-color: #ffffff; - padding: 1em 0em 1em 0em; -} - -footer { - background-color: #ffffff; - margin: 0em auto 1em auto; - padding: 1em 0em 1em 0em; -} - -article { - line-height: 1.625; - margin: 0em auto 0em auto; - padding: 1em; -} - -section { - margin: 0em auto 0em auto; -} - - -/* Blocks */ -audio, video { - display: block; - height: 100%; - margin: 1em auto; - max-width: 100%; - text-align: center; - width: 100%; -} - -aside { - color: #4d4d4c; - font-size: 0.75em; - padding: 1em 0; - width: 100%; -} - -blockquote { - color: #282a2e; - font-family: serif; - margin: 0 auto 0 auto; - width: 80%; -} - -canvas { - display: block; - margin: 0em auto 0em auto; - padding: 0em; -} - -p { - font-size: 1.25em; - margin: 0; -} - -ol, -ul { - font-size: 1.25em; - margin: 0; -} - -ul ul, ul ol, ol ul, ol ol { - font-size: 1em; -} - -h1 { - font-size: 2.5em; - hyphens: none; - margin: 0em; - text-align: center; -} - -h2 { - font-size: 2em; - margin: 1em 0; - text-align: center; -} - -h3 { - font-size: 1.5em; - margin: 0em; -} - -h4 { - font-size: 1em; - margin: 0em; -} - -iframe { - border: 0; - height: 100%; - left: 0; - position: absolute; - top: 0; - width: 100%; -} - -img { - height: auto; - max-width: 100%; -} - -table { - border-collapse: collapse; - border: 3px solid; - max-width: 100%; - width: 100%; -} - -td, -th { - border: 1px solid; -} - -/* Inline */ -a { - color: #4271ae; - text-decoration: none; -} - -a:hover { - color: #3e999f; -} - -a:visited { - color: #8959a8; - text-decoration: none; -} - -blockquote > cite { - color: #4d4d4c; - margin: 0 auto 0 auto; -} - -code { - font: 1em, monospace; -} - -pre { - margin: 0; -} - -pre > code { - background-color: #efefef; - display: block; - padding: 0em 0.5em 0em 0.5em; - white-space: pre-wrap; - word-wrap: break-word; -} - -/* Classes */ -.bold { - font-style: bold; -} - -.bordered { - border: solid 3px; - box-sizing: border-box; - max-width: 100%; - padding: 1em; -} - -.entry-nav { - display: grid; - grid-template-columns: 1fr 1fr; - margin: 1em 0em 0em 0em; -} - -.caption { - display: block; - font-size: 0.75em; - margin: 0 auto 0 auto; - text-align: center; - width: 80%; -} - -.excerpt { - color: #222244; - display: block; - margin: 0 auto 0 auto; - font-size: 1em; - width: 100%; -} - -.hlist li { - display:inline-block; -} - -.hlist li:before { - content: "|"; - font-size: 1em; -} - -.hlist li:first-child:before { - content: ''; -} - -.nobullets { - list-style-type: none; -} - -.img-center { - display: block; - margin: 0 auto; - width: 100%; -} - -.img-center-small { - display: block; - margin: 0 auto; - width: 75%; -} - -.img-round { - border-radius: 50%; -} - -.italic { - font-style: italic; -} - -.nav { - font-size: 1.25em; - text-align: center; -} - -.poetry { - color: #4d4d4c; - display: block; - hyphens: none; - margin: 0 auto 0 auto; - width: 80%; -} - -.text-center { - text-align: center; -} - -.text-large { - font-size: 1.5em; -} - -.text-normal { - font-size: 1em; -} - -.text-right { - text-align: right; -} - -.text-right-if-wide { - text-align: left; -} - -.text-small { - font-size: 0.75em; -} - -.split-row { - display: grid; - grid-template-columns: auto; - max-width: 100%; -} - -.timeline-row-single { - display: grid; - grid-column-gap: 1em; - grid-template-columns: 1fr; - line-height: 1.25; - margin: 1em 0em 0em 0em; - padding: 0; -} - -.timeline-row { - display: grid; - grid-column-gap: 1em; - grid-template-columns: 1fr; - grid-template-rows: min-content; - line-height: 1.25; - margin: 1em 0em 0em 0em; - padding: 0; -} - -.video-container { - padding-bottom: 56.25%; /* 16:9 */ - position: relative; - width: 100%; -} - -.wide64 { - align-items: stretch; - display: flex; - flex-direction: column; - gap: 1em; - justify-content: center; - margin: 0em auto 1em auto; - max-width: 64ch; -} - -.wide64 > a, -.wide64 > img, -.wide64 > video { - align-self: center; -} - -.wide64 > h2 { margin: 1em 0; } - -.wide128 { - max-width: 128ch; -} - -/* Background Colors */ -.bg-black { - background-color: #1d1f21; -} - -.bg-blue { - background-color: #4271ae; -} - -.bg-brown { - background-color: #a3685a; -} - -.bg-cyan { - background-color: #3e999f; -} - -.bg-gray0 { - background-color: #efefef; -} - -.bg-gray1 { - background-color: #e0e0e0; -} - -.bg-gray2 { - background-color: #d6d6d6; -} - -.bg-gray3 { - background-color: #8e908c; -} - -.bg-gray4 { - background-color: #969896; -} - -.bg-gray5 { - background-color: #4d4d4c; -} - -.bg-gray6 { - background-color: #282a2e; -} - -.bg-green { - background-color: #718c00; -} - -.bg-orange { - background-color: #f5871f; -} - -.bg-purple { - background-color: #8959a8; -} - -.bg-red { - background-color: #c82829; -} - -.bg-white { - background-color: #ffffff; -} - -.bg-yellow { - background-color: #eab700; -} - -/* Border Colors */ -.br-black { - border-color: #1d1f21; -} - -.br-blue { - border-color: #4271ae; -} - -.br-brown { - border-color: #a3685a; -} - -.br-cyan { - border-color: #3e999f; -} - -.br-gray0 { - border-color: #efefef; -} - -.br-gray1 { - border-color: #e0e0e0; -} - -.br-gray2 { - border-color: #d6d6d6; -} - -.br-gray3 { - border-color: #8e908c; -} - -.br-gray4 { - border-color: #969896; -} - -.br-gray5 { - border-color: #4d4d4c; -} - -.br-gray6 { - border-color: #282a2e; -} - -.br-green { - border-color: #718c00; -} - -.br-orange { - border-color: #f5871f; -} - -.br-purple { - border-color: #8959a8; -} - -.br-red { - border-color: #c82829; -} - -.br-white { - border-color: #ffffff; -} - -.br-yellow { - border-color: #eab700; -} - -/* Colors */ -.black { - color: #1d1f21; -} - -.blue { - color: #4271ae; -} - -.brown { - color: #a3685a; -} - -.cyan { - color: #3e999f; -} - -.gray0 { - color: #efefef; -} - -.gray1 { - color: #e0e0e0; -} - -.gray2 { - color: #d6d6d6; -} - -.gray3 { - color: #8e908c; -} - -.gray4 { - color: #969896; -} - -.gray5 { - color: #4d4d4c; -} - -.gray6 { - color: #282a2e; -} - -.green { - color: #718c00; -} - -.orange { - color: #f5871f; -} - -.purple { - color: #8959a8; -} - -.red { - color: #c82829; -} - -.white { - color: #ffffff; -} - -.yellow { - color: #eab700; -} - -/* Media */ -@media print { - h1, h2, h3, h4, h5, h6 { - page-break-after: avoid; - } -} - -@media only screen and (min-width: 1px) { - body { - max-width: 95%; - } -} - -@media only screen and (min-width: 600px) { - body { - max-width: 95%; - } - .timeline-row { - grid-template-columns: 1fr 1fr; - } -} - -@media only screen and (min-width: 800px) { - body { - max-width: 80%; - } - .timeline-row { - grid-template-columns: 1fr 1fr; - } -} - -@media only screen and (min-width: 1000px) { - body { - max-width: 66%; - } - .split-row { - grid-template-columns: auto auto; - } - .text-right-if-wide { - text-align: right; - } - .timeline-row { - grid-template-columns: 1fr 1fr; - } -} - -@media only screen and (min-width: 1200px) { - body { - max-width: 50%; - } -} diff --git a/cmd/web/site/static/js/minotaur/direction.js b/cmd/web/site/static/js/minotaur/direction.js @@ -1,22 +0,0 @@ -/* eslint-disable no-bitwise */ - -export const up = 1 << 1; -export const right = 1 << 2; -export const down = 1 << 3; -export const left = 1 << 4; -export const clockwise = [up, right, down, left]; - -// random returns a shuffled array of the four directions. -export const random = () => [up, right, down, left].sort( - () => Math.random() - 0.5, -); - -// opposite returns the direction opposite to the passed direction. -export const opposite = (d) => { - if (d === up) return down; - if (d === right) return left; - if (d === down) return up; - if (d === left) return right; - - throw new Error(`unrecognized direction ${d}`); -}; diff --git a/cmd/web/site/static/js/minotaur/game.js b/cmd/web/site/static/js/minotaur/game.js @@ -1,137 +0,0 @@ -import * as direction from '/static/js/minotaur/direction.js'; - -// Game represents a maze game with player interaction. -export default class Game { - constructor({ document, maze }) { - this.document = document; - this.escaped = false; - this.initialized = false; - this.intervals = { score: null, minotaur: null }; - this.killed = false; - this.maze = maze; - this.pressed = { - down: false, - left: false, - right: false, - space: false, - up: false, - }; - this.score = 0; - - // Add the event listeners. - document.addEventListener('keydown', (e) => this.keyDownHandler(e), false); - document.addEventListener('keyup', (e) => this.keyUpHandler(e), false); - } - - // End handles the game over condition. - end() { - // Clear the intervals. - clearInterval(this.intervals.score); - clearInterval(this.intervals.minotaur); - - // Disable the event handlers. - this.document.removeEventListener('keydown', this.keyDownHandler, false); - this.document.removeEventListener('keyup', this.keyUpHandler, false); - - // Display the game over text. - if (this.escaped) { - this.document.getElementById('game-over').innerHTML = `You escaped the labyrinth!\nScore: ${this.score}.`; - } - if (this.killed) { - this.document.getElementById('game-over').innerHTML = `You were slain by the Minotaur. Score: ${this.score}.`; - } - - // Render the last frame. - return this.maze.render(); - } - - // init starts the game score and the Minotaur's movement. - init() { - // Start measuring the player's score. - this.intervals.score = setInterval(() => { this.score += 1; }, 1000); - - // Move the minotaur. - this.intervals.minotaur = setInterval(() => this.maze.moveMinotaur(), 250); - - this.initialized = true; - } - - // keyDownHandler handles keydown events. - keyDownHandler(e) { - if (e.code === 'ArrowUp') { - this.pressed.up = true; - return e.preventDefault(); - } - if (e.code === 'ArrowRight') { - this.pressed.right = true; - return e.preventDefault(); - } - if (e.code === 'ArrowDown') { - this.pressed.down = true; - return e.preventDefault(); - } - if (e.code === 'ArrowLeft') { - this.pressed.left = true; - return e.preventDefault(); - } - if (e.code === 'Space') { - this.pressed.space = true; - return e.preventDefault(); - } - - return null; - } - - // keyUpHandler handles keyup events. - keyUpHandler(e) { - if (e.code === 'ArrowUp') this.pressed.up = false; - if (e.code === 'ArrowRight') this.pressed.right = false; - if (e.code === 'ArrowDown') this.pressed.down = false; - if (e.code === 'ArrowLeft') this.pressed.left = false; - if (e.code === 'Space') this.pressed.space = false; - } - - // run executes the game loop. - run() { - if (!this.initialized) this.init(); - - // Render the solution. - if (this.pressed.space) { - this.maze.setEscapePath(); - } else { - this.maze.clearEscapePath(); - } - - // Move Theseus. - let dir = null; - if (this.pressed.up) dir = direction.up; - if (this.pressed.right) dir = direction.right; - if (this.pressed.down) dir = direction.down; - if (this.pressed.left) dir = direction.left; - if (dir) { - this.maze.moveTheseus({ d: dir }); - - // Prevent the user from moving more than one cell per keypress. - this.pressed.up = false; - this.pressed.right = false; - this.pressed.down = false; - this.pressed.left = false; - this.pressed.space = false; - } - - // Render the maze. - this.maze.render(); - - // Check for game over conditions. - if (this.maze.escaped()) { - this.escaped = true; - return this.end(); - } - if (this.maze.killed()) { - this.killed = true; - return this.end(); - } - - return requestAnimationFrame(() => this.run()); - } -} diff --git a/cmd/web/site/static/js/minotaur/maze.js b/cmd/web/site/static/js/minotaur/maze.js @@ -1,370 +0,0 @@ -/* eslint-disable no-bitwise, no-continue */ -import * as direction from '/static/js/minotaur/direction.js'; - -// Maze represents a maze with: -// - A player character, Theseus, -// - An enemy, the Minotaur, which chases the player. -// - An exit from the maze. -export default class Maze { - constructor({ canvas, padding = 1, side = 24 }) { - this.canvas = canvas; - this.exit = null; - this.height = Math.floor((canvas.height - (padding * 2)) / side); - this.layout = []; - this.minotaur = null; - this.side = side; - this.solution = null; - this.theseus = null; - this.width = Math.floor((canvas.width - (padding * 2)) / side); - - // Generate the maze. - for (let i = 0; i < this.height; i += 1) { - this.layout[i] = []; - for (let j = 0; j < this.width; j += 1) { - this.layout[i][j] = 0; - } - } - // Connect the cells. - const stack = [{ x: 0, y: 0 }]; - const visited = { '0,0': true }; - while (stack.length > 0) { - const current = stack[stack.length - 1]; - let found = false; - - const rd = direction.random(); - for (const d of rd) { - // x and y are the coordinates of the neighboring cell. - const { x, y } = this.coordinateAtDirection(current, d); - - // Ignore coordinates that are out of bounds. - if (!this.inBounds({ x, y })) { - continue; - } - - // Check if we've already visited this cell. - // If we have, skip it. - const key = `${x},${y}`; - if (visited[key]) continue; - - // If we've found a new cell, mark is at visited. - // Throw it on the stack to continue generating from it. - found = true; - visited[key] = true; - stack.push({ x, y }); - - // Connect the cells. - // Set the current cell. - this.layout[current.y][current.x] |= d; - // Set the neighbor. - this.layout[y][x] |= direction.opposite(d); - - break; - } - - // Remove expended cells. - if (!found) stack.pop(); - } - - // Set the location of the maze exit. - this.exit = this.randomCell(); - - // Set the location of Theseus. - this.theseus = this.randomCell(); - - // Set the location of the Minotaur. - this.minotaur = this.randomCell(); - } - - // clearEscapePath remove the maze solution. - clearEscapePath() { - this.solution = null; - } - - // coordinateAtDirection returns the coordinates reached with the passed in - // starting point and direction. - coordinateAtDirection({ x, y }, d) { - if (d === direction.up) return { x, y: y - 1 }; - if (d === direction.right) return { x: x + 1, y }; - if (d === direction.down) return { x, y: y + 1 }; - if (d === direction.left) return { x: x - 1, y }; - - throw new Error(`unrecognized direction ${d}`); - } - - // escaped returns whether Theseus has reached the exit. - escaped() { - if (this.exit.x !== this.theseus.x) return false; - if (this.exit.y !== this.theseus.y) return false; - return true; - } - - // hasDir returns whether a particular cell has a connection to the - // specified direction. - hasDir({ x, y }, d) { - return (this.layout[y][x] & d) !== 0; - } - - // inBounds returns whether or not the provided coordinates exist in the maze. - inBounds({ x, y }) { - if (x < 0 || y < 0 || x >= this.width || y >= this.height) return false; - return true; - } - - // killed returns whether the Minotaur has reached Theseus. - killed() { - if (this.minotaur.x !== this.theseus.x) return false; - if (this.minotaur.y !== this.theseus.y) return false; - return true; - } - - // openDirections returns the open directions for a cell. - openDirections({ x, y }) { - const directions = []; - - for (const d of direction.clockwise) { - if (this.hasDir({ x, y }, d)) { - directions.push(d); - } - } - - return directions; - } - - // moveMinotaur sets the new location of the Minotaur. - moveMinotaur() { - // const dirs = this.openDirections(this.minotaur); - // if (dirs.length === 1) { // Deadened - // this.lastPosition = this.minotaur; - // this.minotaur = this.coordinateAtDirection(this.minotaur, dirs[0]); - // return; - // } - - // const randomDirections = dirs.sort(() => Math.random() - 0.5); - // for (const d of randomDirections) { - // // Don't go to the last position. - // const next = this.coordinateAtDirection(this.minotaur, d); - // if (this.lastPosition.x === next.x && this.lastPosition.y === next.y) { - // continue; - // } - // // Go to the new direction. - // this.lastPosition = this.minotaur; - // this.minotaur = next; - // return; - // } - - const { path } = this.solve({ start: this.minotaur, end: this.theseus }); - // Move the Minotaur to the next cell in the path. - if (path && path.length >= 2) this.minotaur = path[1]; - } - - // moveTheseus moves Theseus in the provided direction, if it is valid. - moveTheseus({ d }) { - const next = this.coordinateAtDirection(this.theseus, d); - const hasDir = this.hasDir(this.theseus, d); - const inBounds = this.inBounds(next); - - if (hasDir && inBounds) this.theseus = next; - } - - // randomCell returns a random cell in the maze. - randomCell() { - return { - x: Math.floor(Math.random() * this.width), - y: Math.floor(Math.random() * this.height), - }; - } - - // render draws the maze onto the canvas. - render() { - const ctx = this.canvas.getContext('2d'); - const pad = Math.floor((this.canvas.width - (this.width * this.side)) / 2); - const thickness = Math.floor(this.side / 5); - - // cellX and cellY track the top-right edge of the current cell to draw. - let cellX = pad; - let cellY = pad; - - // Clear the canvas. - ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - - // Draw the bottom and right edges. - // The top and left edges will be drawn per cell. - ctx.strokeStyle = '#000000'; - ctx.lineWidth = thickness; - - // Bottom - ctx.beginPath(); - ctx.moveTo( - cellX - Math.floor(thickness / 2), - cellY + (this.side * this.height), - ); - ctx.lineTo( - cellX + (this.side * this.width) + Math.floor(thickness / 2), - cellY + (this.side * this.height), - ); - ctx.stroke(); - // Right - ctx.beginPath(); - ctx.moveTo( - cellX + (this.side * this.width), - cellY - Math.floor(thickness / 2), - ); - ctx.lineTo( - cellX + (this.side * this.width), - cellY + (this.side * this.height), - ); - ctx.stroke(); - - // Draw the cells. - for (let y = 0; y < this.height; y += 1) { - for (let x = 0; x < this.width; x += 1) { - const cell = this.layout[y][x]; - - // Draw the cell borders. - // A top edge will work as a bottom edge, except on the bottom row. - // A left edge will work as a right edge, except on the right column. - // We've handled the special cases above. - ctx.strokeStyle = '#000000'; - ctx.lineWidth = thickness; - - // Top - if ((cell & direction.up) === 0 || y === 0) { - ctx.beginPath(); - ctx.moveTo(cellX, cellY); - ctx.lineTo(cellX + this.side, cellY); - ctx.stroke(); - } - - // Left - if ((cell & direction.left) === 0 || x === 0) { - ctx.beginPath(); - // Extend the edge on the upper-left cell to draw in the corner. - if (y === 0) { - ctx.moveTo(cellX, cellY - Math.floor(thickness / 2)); - } else { - ctx.moveTo(cellX, cellY); - } - ctx.lineTo(cellX, cellY + this.side); - ctx.stroke(); - } - - // Fill the cell area. - ctx.beginPath(); - if (this.solution !== null && this.solution.cells[`${x},${y}`]) { - ctx.fillStyle = '#4271ae'; - } else { - ctx.fillStyle = '#efefef'; - } - ctx.lineWidth = 1; - ctx.rect(cellX, cellY, this.side, this.side); - ctx.fill(); - - // Extend the bottom right edge on cells connected down and right. - // This needs to be done after the cell area is filled, or this - // line will be drawn over. - if ((cell & direction.down) !== 0 && (cell & direction.right) !== 0) { - ctx.lineWidth = thickness; - ctx.beginPath(); - ctx.moveTo( - cellX + (this.side - Math.floor(thickness / 2)), - cellY + this.side, - ); - ctx.lineTo(cellX + this.side, cellY + this.side); - ctx.stroke(); - ctx.lineWidth = 1; - } - - // Draw special cell states -- exit, Theseus, Minotaur. - // The order matters here -- if these overlap, they are drawn over - // each other. - if (x === this.exit.x && y === this.exit.y) { - ctx.beginPath(); - ctx.fillStyle = '#718c00'; - ctx.rect( - cellX + Math.floor(this.side / 4), - cellY + Math.floor(this.side / 4), - Math.floor(this.side / 2), - Math.floor(this.side / 2), - ); - ctx.fill(); - } - - // Draw Theseus. - if (x === this.theseus.x && y === this.theseus.y) { - ctx.beginPath(); - ctx.fillStyle = '#8959a8'; - ctx.arc( - cellX + Math.floor(this.side / 2), - cellY + Math.floor(this.side / 2), - Math.floor(this.side / 4), - 0, - Math.PI * 2, - ); - ctx.fill(); - } - - // Draw the Minotaur. - if (x === this.minotaur.x && y === this.minotaur.y) { - ctx.beginPath(); - ctx.fillStyle = '#c82829'; - ctx.arc( - cellX + Math.floor(this.side / 2), - cellY + Math.floor(this.side / 2), - Math.floor(this.side / 4), - 0, - Math.PI * 2, - ); - ctx.fill(); - } - - // Increment to the next cell. - cellX += this.side; - } - - // Increment to the next row. - cellX = pad; - cellY += this.side; - } - } - - // setEscapePath sets the solution for the maze. - setEscapePath() { - this.solution = this.solve({ start: this.theseus, end: this.exit }); - } - - // solve uses breadth-first search to find a path between start and end. - solve({ start, end }) { - const q = [{ path: [start] }]; - - while (q.length > 0) { - const search = q.shift(); - const { x, y } = search.path[search.path.length - 1]; - - // If we've reach the end, we're done. - if (x === end.x && y === end.y) { - return { - path: search.path, - cells: search.path.reduce((acc, v) => { acc[`${v.x},${v.y}`] = true; return acc; }, {}), - }; - } - - // Find the next cell. - // If the cell is in bounds and not a backtrack, add it to the path. - // Then, add the candidate path to the queue. - const cell = this.layout[y][x]; - let previous = { x: null, y: null }; - if (search.path.length > 2) previous = search.path[search.path.length - 2]; - - for (const d of direction.clockwise) { - if ((cell & d) !== 0) { - const next = this.coordinateAtDirection({ x, y }, d); - const inBounds = this.inBounds(next); - const notPrevious = !(next.x === previous.x && next.y === previous.y); - if (inBounds && notPrevious) q.push({ path: [...search.path, next] }); - } - } - } - - return null; - } -} diff --git a/cmd/web/site/static/public.key b/cmd/web/site/static/public.key @@ -1,51 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQINBFfQqo0BEAC9CtjDP2jNRXVNPmcB6y0F4qY5i25MM/YYaI/jsrd8lWmJ3Ucc -lvfmtDYh1U7c+jp2teBl+wrq4SKQkx/DWZg6hppzHsGzujaYr8U5cO4xw/M8IizM -jm7GP9DllODXv00usyoweLQa+1G7Ai0aK7K2GP2neM2r8WEaaZmm/YHmEg+iMK2R -uAjZIfCBVhE653D4i+gs6gAr3lzMRzvDqeNqVdCIJm9zQsLSsjvhYTBLKStgNdOF -bfamXuTVTsnMlMXWDaNIujDHjbp6sesccJbeFE9wkMM778V5wLvuJP8Sl/EawYWo -RKOHBuXuAb7G2uVvKhQnMfXaXpgFRKgYN6uczH4dhNgYyk0GHdEf6ZF4eUJmPcxE -EJBK5tHVvP2r0AXoHV7qMIqWvPOsZFSuZM3KZZvy3ETIRbq7NLZ/TsVugEeIZX9L -36IFoNlpUcdJr6LLz8roqSj7CLCKDKPeHoq/3Bi9Lw2k12Sk4RBvYOeoIbanh2dp -RFoUVrIBB9WnExnigomSkEINfU2blK4F7jmYFivqudQps1qVlQVwBKuScKjFZbR2 -jNaYNNZV1asv2//bpEXmJg/oZI1NXBFIjSmFre1SH7jVEU0eze99pVg1Iu3Ng288 -Xv+LtF6X+JevGSIP3TFYQogIPK23BbQtx5Cd9FjW8ecET5Np5T1n7M1MNwARAQAB -tCZEYXZpZCBXZW4gUmljY2FyZGktWmh1IDxkd3J6QGR3cnoubmV0PokCOAQTAQIA -IgUCV9CqjQIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ/KbyhV3VRjOZ -PRAAtV3fXd5Ng4+lxdqxfRpWwaTB6OnJyp5JwNwZxTDCoZA/aHLiROd8VnfRsbx5 -v5ed/HzZAFSIy+mxRxxkiGaI2QBU8tXT3gXLcY8Bru/k3dd7sgxYrq7QBjOTIsX+ -PwTeqixDFGGp6BqBYONM3VZr3dVKLrz5wPhwEYetVezHOcCGI/RphYtUK6mDo/ni -uaHX6dA/YKEP4BlsOOTAj3Uw85e5RKjAZvuwExeDWNM2Vft59jsursh10blqJBmh -oIoCbAR0w7YPCg5EbHKLhudgQY6/zczlqhoQGd7bZDzKAWfxWO9yVtj0x6RVxIlg -3nS16tS0QsfU51pWZOKDFq2N2A4GYZBUvR5bgRbatlAb2wlsFfGI0eRgY6vK+Nss -ov3NoQKQbfwTNmIlRPlVohzRWUAQP0M10fWJ/AMCvK4hhncFibTbTX9mk0TyK/bU -nwlr3HPfBdenkPmXiNuM1HOMhTK++ry2OHF7qBSvPdzgsi1GulmoXo7xC74nJ427 -n4bgtSeIZCQ6LK7IM6c+SyjB83j7XDl/PT8ZRjQ6c0lJdSEXdrn6605Hvgl86gPh -mUaJbCbDDhQBQMsz1guVX7t4Vzq3wUur+tsbUItA0KF3ib3uogDWuUD1wc0wb4Ug -mPw4YDcZ8v8/UvaO/6fA+qRiDytvG1B4UCuMXgjErKlkxfi5Ag0EV9CqjQEQALc6 -SnPZgnQwXcCSBzXDExweIVXgRjyMCMJQOiKjuqWAD0ErHmyAgUjqHrQ3hG7EZQYP -rabsxWnaB7xJhNP1V+EKl9oCOFp+3O1fe8GakHUkIgtH+sCcm2ucF2VjyyeGS628 -fMlA6CXECIxoXXqXD8mZcP5rMQH/jymqBTv6FnXoAx4Ck7hUZ2KYTDU5asZUPXL1 -gXKuKNq3Ck9NGP/l7q9dQTkRcUzDcF1OC69h8/5mVflrsYHRqZ1cfIBCpEAvQIUS -3AmE8DqAMR0sScHG7mMMRgsGCvM7QkZLCN9yGLvmpuJwJa+Rk/hqA+OjOXL1nMGI -3aI24a7oMj9MxfM7tIqzSNbopLvfSc5cITHLc1fSbvMN1oeYbQp/oDQkJGQ4w5hQ -o/rTwp44C7rcF0PwMwiukCWMZ4pXJuBieolgFseDQq2Is2i/+chrvqyalts7swY9 -5KejJKgR7l8u09WuqX7BnYwsVX4bZPcx7OarECkzd87cHEViUpz+VjR9HmUVnerH -22fFZPl20CpgTeU4wozSKzKArQEZezfB5hCEGT6kZzAjEsBlYp1yizsWZi0PpGVT -InKRe4gkqYgpPAPhOWXhl/FjnF92RAqO0v2bctxKO2OjIcSi4/qyWvVsluK7aOIv -AEm9QKcsiTvKa5R/5SW3vdZNiftG+SVWIxbA/6MvABEBAAGJAh8EGAECAAkFAlfQ -qo0CGwwACgkQ/KbyhV3VRjNR8w/7B14RPKsQNHbnf7pnGSAj0znnAFj5XIyyIZAz -tt0O5QIJ4sCUlPkAjvAz8HzgiuIHOiiASNu80SiGI0SFMD5V16gyQFWgqSOsrifb -r7uAoOtJTbXOU7IgzJWlUcZVnzSMghwP642z3tdvlhruCv40qBGXuYouZproz+bL -UsSNwzh3RrujXCiwHzoG2RRHwt1cwXG5ndTX4fFQMQn7rBGsLrAlN/7MTJ5NNmuT -TvZZyPvFld+7YVy9ANNwv6wj7VDXoC8jF8yJeMf5aeIpUmyQrufCXmNpPd8AFUVR -rWDh3z/zVZo6MWqbAk9CJsRbL2/FyOoVmzxOvYZgP7W4ObvvjAxEVkRctlb4QkGe -Agf1PgPlOBXJj+fbKEF+p7raNKyOZSrLWXkoHn/I6GenbM1nbUcV/IZLFRqlcRZJ -heMzRNG7J7LxWM586DMprbEx0Fp76hzySS9GONIDsAaLaT6CS3Lzxa1gwikWru1X -XrkdJjiBoVckBt/mRMCqznqBgQzakACq5sZrktZp6hYt2AZOqzmsS9MaU8KRstNx -2RGw01G/F+oRXifAx0Zr4cBQJUTOK37FE9MkCWTUHDTudDCG0PqI6crSyLdt5/oj -Ac+Mqteh53SoYjmohBvgbK5mjAjlLgy0/3vVJ3DOcGmp+Y36QUiju8MoHv0g82Z9 -Snd72y4= -=MO/l ------END PGP PUBLIC KEY BLOCK----- diff --git a/cmd/web/site/status.go b/cmd/web/site/status.go @@ -1,14 +0,0 @@ -package site - -import ( - "net/http" -) - -func (s *Site) status(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - w.WriteHeader(http.StatusOK) -} diff --git a/cmd/web/site/templates/static/base.gohtml b/cmd/web/site/templates/static/base.gohtml @@ -1,16 +0,0 @@ -{{ define "base" }} - <!doctype html> - <html lang="en"> - <head> - <meta charset="utf-8"> - <meta name="viewport" - content="width=device-width, initial-scale=1, shrink-to-fit=no"> - <title>dwrz.net</title> - <meta name="author" content="David Wen Riccardi-Zhu"> - <link rel="stylesheet" href="/static/css/dwrz.css"/> - </head> - <body> - {{ template "body" . }} - </body> - </html> -{{ end }} diff --git a/cmd/web/site/templates/static/body.gohtml b/cmd/web/site/templates/static/body.gohtml @@ -1,5 +0,0 @@ -{{ define "body" }} - {{ template "header" . }} - {{ template "main" . }} - {{ template "footer" . }} -{{ end }} diff --git a/cmd/web/site/templates/static/contact.gohtml b/cmd/web/site/templates/static/contact.gohtml @@ -1,11 +0,0 @@ -{{ define "contact" }} - <article class="wide64"> - <p> - <a href="mailto:dwrz@dwrz.net">dwrz@dwrz.net</a> - <br> - <a href="/static/public.key"> - 30EA CE33 766E 650E 2A39 DE30 FCA6 F285 5DD5 4633 - </a> - </p> - </article> -{{ end }} diff --git a/cmd/web/site/templates/static/cv-education.gohtml b/cmd/web/site/templates/static/cv-education.gohtml @@ -1,60 +0,0 @@ -{{ define "cv-education" }} - <h2 class="bg-blue white" id="education">Education</h2> - <div class="split-row"> - <h3>Self-directed Studies</h3> - <h3 class="text-right-if-wide"> - <a href="https://www.recurse.com/">Recurse Center</a> - </h3> - </div> - <h4 class="gray3 italic">October 2022 – February 2023 (W1 2022)</h4> - - <hr class="gray0"> - <div class="split-row"> - <h3>Advanced Software Engineering Immersive</h3> - <h3 class="text-right-if-wide"> - <a href="https://www.hackreactor.com/">Hack Reactor</a> - </h3> - </div> - <h4 class="gray3 italic">October 2017 – January 2018</h4> - - <hr class="gray0"> - <div class="split-row"> - <h3>Full Stack Web Development Certification</h3> - <h3 class="text-right-if-wide"> - <a href="https://www.freecodecamp.org/">Free Code Camp</a> - </h3> - </div> - <h4 class="gray3 italic">October 2016 – August 2017</h4> - - <hr class="gray0"> - <div class="split-row"> - <h3>Juris Doctor</h3> - <h3 class="text-right-if-wide"> - <a href="https://www.stjohns.edu/law"> - St. John's University School of Law - </a> - </h3> - </div> - <h4 class="gray3 italic">August 2011 – May 2014</h4> - <h4 class="gray3 italic">Bar Admission, NY 2015</h4> - - <hr class="gray0"> - <div class="split-row"> - <h3> - Bachelor of Arts (Philosophy, Italian Studies) - </h3> - <h3 class="text-right-if-wide"> - <a href="https://www.wesleyan.edu/">Wesleyan University</a> - </h3> - </div> - <h4 class="gray3 italic">August 2005 – May 2009</h4> - - <hr class="gray0"> - <div class="split-row"> - <h3>Diploma</h3> - <h3 class="text-right-if-wide"> - <a href="https://www.horacemann.org/">Horace Mann School</a> - </h3> - </div> - <h4 class="gray3 italic">August 2001 – May 2005</h4> -{{ end }} diff --git a/cmd/web/site/templates/static/cv-experience.gohtml b/cmd/web/site/templates/static/cv-experience.gohtml @@ -1,232 +0,0 @@ -{{ define "cv-experience" }} - <h2 class="bg-blue white" id="experience">Experience</h2> - <div class="split-row"> - <h3> - Founding Engineer - </h3> - <h3 class="text-right-if-wide"> - <a href="https://www.museumexchange.com/">Museum Exchange</a> - </h3> - </div> - <h4 class="gray3 italic">November 2023 – Present</h4> - <ul> - <li> - As sole engineer, took over aging Go and React based MVP and built a new - and expanded implementation in Go. - <ul> - <li> - Built donation management system featuring object listing, catalogue - viewing, and proposal and donation workflows. - </li> - <li> - Developed comprehensive admin portal including donation kanban board, - reporting systems, and metrics dashboards. - </li> - <li> - Implemented asynchronous worker infrastructure for bulk uploads, - metrics, report generation, and notifications. - </li> - <li>Integrated Dropbox Sign for digital agreements.</li> - </ul> - <li> - Architected new database schema, migrated data, and refactored - queries to utilize MongoDB aggregations. - </li> - <li> - Implemented responsive frontend components and templates with Bootstrap - and custom CSS. - </li> - <li>Managed AWS infrastructure and deployment of services.</li> - <li> - Mentored interns on backend and frontend development and best - practices; conducted code review on their PRs. - </li> - </ul> - <div class="split-row"> - <h3> - Founder - </h3> - <h3 class="text-right-if-wide"> - <a href="https://www.chimeric.al/">Chimerical LLC</a> - </h3> - </div> - <h4 class="gray3 italic">March 2023 – Present</h4> - <ul> - <li> - Iterating on an E-commerce platform written in Go and PostgreSQL; focus - on progressive enhancement and rapid order placement. - </li> - </ul> - <div class="split-row"> - <h3> - Senior Site Reliability Engineer - </h3> - <h3 class="text-right-if-wide"> - <a href="https://www.wish.com/">Wish</a> - </h3> - </div> - <h4 class="gray3 italic">April 2023 – November 2023</h4> - <ul> - <li> - Led incident response as both analyst and coordinator during on-call - rotations. - </li> - <li> - Identified and resolved site performance and security issues. - </li> - <li> - Created weekly site status report generator (Go, incident.io API) and - added support for DORA and other SRE metrics. - </li> - </ul> - <div class="split-row"> - <h3> - Founding Engineer - </h3> - <h3 class="text-right-if-wide"> - <a href="https://www.daybreak.health/">Daybreak Health</a> - </h3> - </div> - <h4 class="gray3 italic">January 2022 – October 2022</h4> - <ul> - <li>Architected and built Daybreak Health API, utilized by its cross-platform React Native app.</li> - <li>Stood up backend development environment, including build scripts, service configuration, and containerization.</li> - <li>Developed database schema, migrations, queries, and Go packages for database models.</li> - <li>Integrated backend with AWS, Salesforce, Slack, Twilio, and Health Gorilla APIs.</li> - <li>Documented API and backend development setup for onboarding engineers. - </li> - <li>Mentored and trained junior engineers and interns.</li> - <li>Coducted code review and maintained the backend monorepo.</li> - </ul> - - <hr class="gray0"> - <div class="split-row"> - <h3>Lead Engineer</h3> - <div> - <h3 class="text-right-if-wide"> - <a href="https://www.gooduncle.com/">Good Uncle</a> - </h3> - </div> - </div> - <h4 class="gray3 italic">January 2018 – January 2022</h4> - <h4 class="gray3"> - Acquired by <a href="https://www.aramark.com">Aramark</a>, May 2019. - </h4> - <ul> - <li>Built backend services, enabling simultaneous cooking and delivery of orders from proprietary vans.</li> - <li>~400K orders and $5M in revenue as of August 2021, with an average delivery time of 25 minutes.</li> - <li>Rearchitected infrastructure to obtain cost reductions of 99% for AWS, 69% for MongoDB, and 85% for CI/CD.</li> - <li>Built control panel web app, utilized by delivery drivers, operations, marketing, and customer support.</li> - <li>Managed development and release of cross-platform React Native app; contributed refactors and bug fixes.</li> - <li>Consolidated the engineering team to two engineers, reducing the largest engineering expenditure.</li> - <li>Built an inventory prediction engine used by the culinary team to estimate the number of meals to prepare.</li> - <li>Integrated backend with university payment systems, including: Atrium, CBORD, and Transact.</li> - <li>Designed and developed a mealplan subscription system in three months, collecting over $500K in revenue.</li> - <li>Created command-line tools to assist the engineering team, including an orders generator to simulate load.</li> - <li>Found and fixed mission critical application and infrastructure bugs while under the pressure of live delivery operations.</li> - <li>Integrated APIs, including: AWS, Big Query, Google Maps, Intercom, Samsara, Slack, Stripe, and Twilio.</li> - </ul> - - <hr class="gray0"> - <div class="split-row"> - <h3>Consultant</h3> - <h3 class="text-right-if-wide"> - <a href="https://www.linkedin.com/company/www.chinaenergyfund.org/"> - China Energy Fund Committee - </a> - </h3> - </div> - <h4 class="gray3 italic">August 2014 – July 2017</h4> - <ul> - <li>Drafted speeches delivered at high-level events on public policy and international relations, including the Internet - Governance Forum, the UN Secretary-General’s High-level Advisory Group on Sustainable Transport, and CEFC - sponsored events on Sino-U.S. relations and sustainable development. - </li> - <li>Composed editorials for publications on sustainability, international relations, internet governance, and China’s economic - development. - </li> - <li>Assembled the preliminary rules and procedures for the award of a $1M energy grant.</li> - <li>As Sherpa, supported Member of the UN Secretary-General’s High-level Advisory Group on Sustainable Transport; contributing to the group’s internal discussions and final outlook report.</li> - <li>Supported Member of the Internet Governance Forum’s Multistakeholder Advisory Group, including the Group’s guidance for the 11th Internet Governance Forum.</li> - </ul> - - <hr class="gray0"> - <div class="split-row"> - <h3>Consultant</h3> - <h3 class="text-right-if-wide"> - FISO Group LLC - </h3> - </div> - <h4 class="gray3 italic">January 2016 – June 2016</h4> - <ul> - <li>Drafted a private sector Commitment Letter to the Sustainable Development Goals, signed by dozens of businesses.</li> - <li>Wrote the concept note, press release, M.C.’s script, and remarks for the UN Assistant Secretary General for Economic Development for the letter’s signing and submission ceremony, held at the United Nations.</li> - </ul> - - <hr class="gray0"> - <div class="split-row"> - <h3>Intern</h3> - <h3 class="text-right-if-wide"> - <a href="https://www.nyc.gov/site/dep/index.page"> - New York City Department of Environmental Protection - </a> - </h3> - </div> - <h4 class="gray3 italic">June 2013 – August 2013</h4> - <ul> - <li>Researched and drafted legal memoranda in support of NYC Air Code revision, SPDES permit modifications, and compliance with state law and consent orders.</li> - <li>Examined legal implications of railbanking proposal, drafted legal memoranda and intergovernmental agreement, and explained railbanking process and provided legal options to Bureau of Water Supply.</li> - <li>Represented the City of New York at environmental administrative hearings, wrote and filed appeals to administrative court decisions, conferenced with opposing counsel and pro se respondents.</li> - </ul> - - <hr class="gray0"> - <div class="split-row"> - <h3>Infantry Assault Marine</h3> - <h3 class="text-right-if-wide"> - <a href="https://www.marines.mil/">United States Marine Corps Reserve</a> - </h3> - </div> - <h4 class="gray3 italic">August 2009 – Jun 2013</h4> - <ul> - <li>Trained in the employment of rockets, military explosives, dynamic breaching, infantry and anti-armor operations.</li> - <li>Supervised, trained, and mentored junior Marines.</li> - <li>Professional achievements: meritorious promotion to Corporal (December 2011), "Excellent" (4.7/5.0) proficiency and conduct marks, Certificate of Commendation (July 2011), completed Scout-Sniper Platoon indoctrination (July 2011), high scorer on Javelin anti-armor missile certification (December 2010), multiple High Physical Fitness Awards.</li> - <li>Training achievements: Company Honor Graduate and Platoon Honor Graduate (Recruit Training, November 2009), Meritorious Mast (School of Infantry, April 2010), “Excellent” (4.8/5.0) proficiency and conduct marks. Consistently placed in leadership positions: Platoon Guide (Basic Marine Platoon, Recruit Training), Assault Section Squad Leader (School of Infantry). - </li> - </ul> - - - <hr class="gray0"> - <div class="split-row"> - <h3>Intern</h3> - <h3 class="text-right-if-wide"> - <a href="https://www.epa.gov/"> - United States Environmental Protection Agency - </a> - </h3> - </div> - <h4 class="gray3 italic">January 2013 – April 2013</h4> - <ul> - <li>Researched and drafted legal memoranda reviewing the Clean Air Act, EPA manuals and regulations, Environmental Appeals Board decisions, and academic literature.</li> - <li>Examined the use of Prevention of Significant Deterioration Best Available Control Technology analysis to promote climate change adaptation.</li> - <li>Attended meetings for the development of EPA Region 2 climate change adaptation strategies.</li> - </ul> - - <hr class="gray0"> - <div class="split-row"> - <h3>Intern</h3> - <h3 class="text-right-if-wide"> - <a href="https://www.un.org/">United Nations</a> - </h3> - </div> - <h4 class="gray3 italic">September 2012 – Decemeber 2012</h4> - <ul> - <li>Prepared preliminary draft of <a href="https://digitallibrary.un.org/record/756820?ln=en">Secretary-General’s report on Rio+20 proposal for High Commissioner for Future Generations</a>.</li> - <li>Researched academic literature, stakeholder submissions, case law and statutes.</li> - <li>Collaborated with NGO directors, political figures, and judges across the globe for research and assessment of the proposal.</li> - <li>Reviewed legal and political implications, highlighting support for and vulnerabilities of the proposal.</li> - <li>Summarized experiences of national examples of similar offices.</li> - <li>Proposed recommendations on possible responses to the proposal, in the context of the existing UN system.</li> - </ul> - -{{ end }} diff --git a/cmd/web/site/templates/static/cv-links.gohtml b/cmd/web/site/templates/static/cv-links.gohtml @@ -1,17 +0,0 @@ -{{ define "cv-links" }} - <h2 class="bg-blue white" id="links">Links</h2> - <ul class="hlist text-center"> - <li><a href="/static/resume.pdf">Résumé</a></li> - <li> - <a href="https://www.linkedin.com/in/dwrz/" target="_blank">LinkedIn</a> - </li> - <li> - <a href="https://github.com/dwrz" target="_blank">Github (Personal)</a> - </li> - <li> - <a href="https://github.com/dwrz-dbhg" target="_blank"> - Github (Daybreak Health) - </a> - </li> - </ul> -{{ end }} diff --git a/cmd/web/site/templates/static/cv-projects.gohtml b/cmd/web/site/templates/static/cv-projects.gohtml @@ -1,55 +0,0 @@ -{{ define "cv-projects" }} - <h2 class="bg-blue white" id="projects">Personal Projects</h2> - <ul> - <li> - <b> - <a href="https://github.com/dwrz/src/tree/trunk/cmd/web"> - dwrz.net - </a> - </b> - — Self-hosted personal website - <span class="gray3">(acme-client, relayd, OpenBSD, Go)</span>. - </li> - <li> - <b><a href="https://github.com/dwrz/vigil">vigil</a></b> - — Self-hosted security cameras with object detection - <span class="gray3"> - (motion, bash, Raspberry Pi, AWS Rekognition, yolov7) - </span>. - </li> - <li>OpenBSD routers <span class="gray3">(dhcpd, pf, unbound)</span>.</li> - <li>FizzBuzz <span class="gray3">(x86 Assembly)</span></li> - <li> - <b> - <a href="https://github.com/dwrz/src/tree/trunk/cmd/minotaur"> - Minotaur - </a> - </b> - — Terminal game <span class="gray3">(Go)</span> - </li> - <li> - <b> - <a href="https://dwrz.net/timeline/2022-10-01/"> - Minotaur - </a> - </b> - — HTML canvas game game <span class="gray3">(JavaScript)</span>. - </li> - <li> - <b> - <a href="https://github.com/dwrz/xv6-riscv/commit/b47a4a40a29b5a29dc36d06c47624b1ec573bce4"> - Free memory system call in xv6 - </a> - </b> - <span class="gray3">(C)</span> - </li> - <li> - <b> - <a href="https://github.com/dwrz/src/tree/trunk/cmd/dqs"> - Diet Quality Score Calculator - </a> - </b> - — Terminal CLI app <span class="gray3">(Go)</span>. - </li> - </ul> -{{ end }} diff --git a/cmd/web/site/templates/static/cv-skills.gohtml b/cmd/web/site/templates/static/cv-skills.gohtml @@ -1,10 +0,0 @@ -{{ define "cv-skills" }} - <h2 class="bg-blue white" id="skills">Skills</h2> - <p> - AWS, bash, Containers, CSS, Go, git, HTML, JavaScript, MongoDB, - PostgresSQL, REST APIs, Schema and System Design - </p> - <p> - C, Emacs Lisp, Linux, Networking, OpenBSD, Python, System Administration - </p> -{{ end }} diff --git a/cmd/web/site/templates/static/cv-summary.gohtml b/cmd/web/site/templates/static/cv-summary.gohtml @@ -1,13 +0,0 @@ -{{ define "cv-summary" }} - <section class="wide64"> - <blockquote> - <p> - Whatever your hand finds to do, do it with your might, for there is no - work, or thought, or knowledge, or wisdom, in the realm of the dead, to - which you are going. - </p> - - <cite>Ecclesiastes 9:10</cite> - </blockquote> - </section> -{{ end }} diff --git a/cmd/web/site/templates/static/cv.gohtml b/cmd/web/site/templates/static/cv.gohtml @@ -1,10 +0,0 @@ -{{ define "cv" }} - <article class="wide128"> - {{ template "cv-summary" }} - {{ template "cv-links" }} - {{ template "cv-skills" }} - {{ template "cv-experience" }} - {{ template "cv-education" }} - {{ template "cv-projects" }} - </article> -{{ end }} diff --git a/cmd/web/site/templates/static/entry.gohtml b/cmd/web/site/templates/static/entry.gohtml @@ -1,22 +0,0 @@ -{{ define "entry" }} - <article class="wide128 entry"> - <h2>{{ .Entry.Title }}</h2> - {{ .Entry.Content }} - <div class="entry-nav"> - <div> - {{ if .Entry.Previous }} - <a href="/timeline/{{ .Entry.Previous.Link }}/"> - {{ .Entry.Previous.Title }} - </a> - {{ end }} - </div> - <div class="text-right"> - {{ if .Entry.Next }} - <a href="/timeline/{{ .Entry.Next.Link }}/"> - {{ .Entry.Next.Title }} - </a> - {{ end }} - </div> - </div> - </article> -{{ end }} diff --git a/cmd/web/site/templates/static/error.gohtml b/cmd/web/site/templates/static/error.gohtml @@ -1,12 +0,0 @@ -{{ define "error" }} - <article> - <h3>Request {{ .RequestId }}</h3> - <p>{{ .Message }}</p> - {{ if .Debug }} - <h3>Error</h3> - <pre><code>{{ .Text }}</code></pre> - <h3>Trace</h3> - <pre><code>{{ .Trace }}</code></pre> - {{ end }} - </article> -{{ end }} diff --git a/cmd/web/site/templates/static/footer.gohtml b/cmd/web/site/templates/static/footer.gohtml @@ -1,5 +0,0 @@ -{{ define "footer" }} - <footer> - {{ template "nav" . }} - </footer> -{{ end }} diff --git a/cmd/web/site/templates/static/header.gohtml b/cmd/web/site/templates/static/header.gohtml @@ -1,5 +0,0 @@ -{{ define "header" }} - <header> - {{ template "nav" . }} - </header> -{{ end }} diff --git a/cmd/web/site/templates/static/home.gohtml b/cmd/web/site/templates/static/home.gohtml @@ -1,27 +0,0 @@ -{{ define "home" }} - <article class="wide64"> - <h1 class="red">David Wen Riccardi-Zhu</h1> - <a href="/static/media/1920/dwrz_20200905T205435_edit.jpg"> - <img alt="Self-portrait while hiking to West Mountain." - class="img-center-small img-round" - src="/static/media/720/dwrz_20200905T205435_edit.jpg"> - </a> - <h1 class="red">朱为文</h1> - <blockquote> - <p>"Seven days ago, I said I was going to leave you. It is customary to write a farewell poem, but I am neither poet nor calligrapher. One of you please inscribe my last words."</p> - <p>His disciples thought he was joking, but one started to write. Hoshin dictated:</p> - <p><em>I came from brilliancy<br> - and return to brilliancy.<br> - What is this?<br></em></p> - <p>The poem was one line short of the customary four, so the disciple said, "Master, we are short by one line."</p> - <p>Hoshin, with the roar of a conquering lion, shouted <strong>Kaa!</strong> and was gone.</p> - <cite><p>The Last Poem of Hoshin, Zen Flesh, Zen Bones</p></cite> - </blockquote> - - <p>I was born in Naples, Italy, my mother's birthplace. My father is from Shanghai, China. In the mid-1990's we moved to Roosevelt Island, in New York City. I attended the United Nations International School, La Scuola d'Italia Guglielmo Marconi, and the Horace Mann School. I went to Wesleyan University for college, where I majored in Philosophy and Italian Studies. After Wesleyan I enlisted in the Marine Corps Reserve, where I served for four years as an Infantry Assault Marine. I studied law at Saint John's University, and was admitted to the New York Bar in 2015. My background is primarily in environmental law and policy. After working in that field for a few years, I switched tracks again; these days I am employed as a software engineer.</p> - - <p>I love nature, time with family, and learning, thinking and living mindfully. I am happiest when I am outdoors, pondering existence, reading books, or being physically active in some way. I don't need much, beyond the basics, to make me happy, and I know I have much to be grateful for. Most of my frustration stems from the excesses of modernity, the spiritual sickness that seems to pervade my species, injustice, and shortsightedness. I worry more about trends than incidences, and care more for the mean than the outliers.</p> - - <p>I believe that life is worth living, that the world is worth fighting for, in human potential, and in the search for knowledge and enlightenment. Since I know I will only be on Earth for so long, at least in this form, these are the causes I hope to advance in my allotted time.</p> - </article> -{{ end }}}} diff --git a/cmd/web/site/templates/static/main.gohtml b/cmd/web/site/templates/static/main.gohtml @@ -1,17 +0,0 @@ -{{ define "main" }} - <main> - {{ if eq .View "contact" }} - {{ template "contact" . }} - {{ else if eq .View "cv" }} - {{ template "cv" . }} - {{ else if eq .View "entry" }} - {{ template "entry" . }} - {{ else if eq .View "error" }} - {{ template "error" . }} - {{ else if eq .View "home" }} - {{ template "home" . }} - {{ else if eq .View "timeline" }} - {{ template "timeline" . }} - {{ end }} - </main> -{{ end }} diff --git a/cmd/web/site/templates/static/nav.gohtml b/cmd/web/site/templates/static/nav.gohtml @@ -1,26 +0,0 @@ -{{ define "nav" }} - {{ $view := .View }} - <nav class="nav"> - {{ if eq $view "home" }} - | <span>home</span> | - {{ else }} - | <a href="/">home</a> | - {{ end }} - {{ if eq $view "timeline" }} - <span>timeline</span> | - {{ else }} - <a href="/timeline/">timeline</a> | - {{ end }} - <a href="https://code.dwrz.net/src/">code</a> | - {{ if eq $view "cv" }} - <span>cv</span> | - {{ else }} - <a href="/cv/">cv</a> | - {{ end }} - {{ if eq $view "contact" }} - <span>contact</span> | - {{ else }} - <a href="/contact/">contact</a> | - {{ end }} - </nav> -{{ end }} diff --git a/cmd/web/site/templates/static/timeline.gohtml b/cmd/web/site/templates/static/timeline.gohtml @@ -1,39 +0,0 @@ -{{ define "timeline" }} - <article class="wide128"> - {{ range .Years }} - <h2 class="bg-green white">{{ .Text }}</h2> - {{ range .Entries }} - {{ if .Cover }} - <div class="timeline-row"> - <div> - <a href="/timeline/{{ .Link }}/"> - <img alt="" src="/static/media/480/{{ .Cover }}"> - </a> - </div> - <div> - <a class="text-large" href="/timeline/{{ .Link }}/"> - {{ .Title }} - </a> - <br> - <span class="text-small gray3"> - {{ .Date.Format "2006-01-02" }} - </span> - </div> - </div> - {{ else }} - <div class="timeline-row-single"> - <div> - <a class="text-large" href="/timeline/{{ .Link }}/"> - {{ .Title }} - </a> - <br> - <span class="text-small gray3"> - {{ .Date.Format "2006-01-02" }} - </span> - </div> - </div> - {{ end }} - {{ end }} - {{ end }} - </article> -{{ end }} diff --git a/cmd/web/site/templates/templates.go b/cmd/web/site/templates/templates.go @@ -1,45 +0,0 @@ -package templates - -import ( - "embed" - "fmt" - "html/template" - "io/fs" - "strings" -) - -//go:embed static/* -var static embed.FS - -const fileType = ".gohtml" - -var funcs = template.FuncMap{} - -func Parse() (*template.Template, error) { - var tmpl = template.New("").Funcs(funcs) - - parseFS := func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() { - return nil - } - - if strings.Contains(path, fileType) { - if _, err := tmpl.ParseFS(static, path); err != nil { - return fmt.Errorf( - "failed to parse template: %v", err, - ) - } - } - - return nil - } - - if err := fs.WalkDir(static, ".", parseFS); err != nil { - return nil, fmt.Errorf("failed to parse templates: %v", err) - } - - return tmpl, nil -}