| 1 | package sheets |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "net/http" |
| 6 | |
| 7 | "amadan.net/rastrillo/rastrillo/jobs" |
| 8 | ) |
| 9 | |
| 10 | // jobPageView is the job status page's payload. |
| 11 | type jobPageView struct { |
| 12 | // Status is the dict rastrillo's own job-status partial reads. |
| 13 | Status map[string]any |
| 14 | // Running drives the noscript meta refresh. It is a separate field |
| 15 | // rather than a lookup into Status because the refresh must be |
| 16 | // emitted ONLY while the job is running: on a finished page it |
| 17 | // reloads forever, which is the failure rastrillo's own docs warn |
| 18 | // about. |
| 19 | Running bool |
| 20 | // PollSeconds is that refresh's interval, kept in step with the |
| 21 | // shim's by coming from the same PageData. |
| 22 | PollSeconds int |
| 23 | } |
| 24 | |
| 25 | // jobStatusData shapes a job for the job-status partial. |
| 26 | // |
| 27 | // Status is converted to a plain string deliberately. The partial |
| 28 | // compares it with {{if eq .Status "running"}}, and template's eq |
| 29 | // refuses to compare a named type (jobs.Status) against an untyped |
| 30 | // string constant — it errors at execute time rather than quietly |
| 31 | // taking the wrong branch, which would be a 500 on every job page and |
| 32 | // nowhere else. |
| 33 | func jobStatusData(d jobs.PageData) map[string]any { |
| 34 | return map[string]any{ |
| 35 | "Name": d.Job.Name, |
| 36 | "Status": string(d.Job.Status), |
| 37 | "Progress": d.Job.Progress, |
| 38 | "Err": d.Job.Err, |
| 39 | "PollURL": d.FragmentPath, |
| 40 | "PollSeconds": d.PollSeconds, |
| 41 | // Set, so a browser that supports it rides server push and |
| 42 | // stops polling. Unset, the shim polls exactly as before, so |
| 43 | // naming it costs nothing where it is not supported. |
| 44 | "PushURL": d.EventsPath, |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | // renderJobPage draws the whole status page. rastrillo 303s away from |
| 49 | // here by itself once the job is done and has somewhere to go, so this |
| 50 | // only ever draws a job that is still running or has failed. |
| 51 | func (a *App) renderJobPage(w http.ResponseWriter, r *http.Request, d jobs.PageData) { |
| 52 | a.render(w, r, "job", titled("Importing a spreadsheet"), http.StatusOK, jobPageView{ |
| 53 | Status: jobStatusData(d), |
| 54 | Running: d.Job.Status == jobs.Running, |
| 55 | PollSeconds: d.PollSeconds, |
| 56 | }) |
| 57 | } |
| 58 | |
| 59 | // renderJobFragment draws the polled partial ALONE — no layout. |
| 60 | // |
| 61 | // Alone is the whole contract: the shim replaces the polling element |
| 62 | // with whatever comes back, so a fragment wrapped in the page layout |
| 63 | // nests a second copy of the page inside the first on every tick. |
| 64 | // |
| 65 | // It executes out of the "job" tree because every page tree is parsed |
| 66 | // with rastrillo's partials alongside this app's own (parsePages), so |
| 67 | // "job-status" is defined there — no second tree, and no template file |
| 68 | // of ours that exists only to hold somebody else's partial. |
| 69 | func (a *App) renderJobFragment(w http.ResponseWriter, _ *http.Request, d jobs.PageData) { |
| 70 | t, ok := pages["job"] |
| 71 | if !ok { |
| 72 | http.Error(w, "Something went wrong.", http.StatusInternalServerError) |
| 73 | return |
| 74 | } |
| 75 | var buf bytes.Buffer |
| 76 | if err := t.ExecuteTemplate(&buf, "job-status", jobStatusData(d)); err != nil { |
| 77 | http.Error(w, "Something went wrong.", http.StatusInternalServerError) |
| 78 | return |
| 79 | } |
| 80 | w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| 81 | w.Write(buf.Bytes()) |
| 82 | } |
| 83 | |