PageSourceSearch

https://splashiq.ai/assets/admin.growth-BTofaoD8.js

js splashiq.ai collected 2026-10-05 12:44:27 UTC 19,704 bytes, 3 lines download raw bytes

1import{Y as S,am as k,Z as F,r,t as c,j as e,I as K,B as l,b as le,p as ce,aD as de,aF as oe,cT as me,cU as he,cN as xe,h as Y,at as ie,L as W,A as Z}from"./index-CzAkNt2W.js";import{u as C}from"./useServerFn-7MfwXlwR.js";import{C as M,a as G,b as P,c as I,d as B}from"./card-DyZ6_8_C.js";import{L as h}from"./label-BwWVuV93.js";import{D as pe,b as ue,c as fe,d as je,e as ge,f as be}from"./dialog-DFazxlVc.js";import{C as ye}from"./checkbox-G3X53UcF.js";import{S as te,a as se,b as ae,c as re,d as U}from"./select-CgG1LT6p.js";import{L as ve}from"./loader-circle-B0UeYsVT.js";import{D as J}from"./download-C5tX2Uho.js";import{H as ne}from"./HelpHint-D0u0IIus.js";import{R as we}from"./refresh-cw-DeA5DQmv.js";import{T as Ne}from"./triangle-alert-Cl8dz-Zm.js";import{R as _e}from"./rotate-ccw-DxxktjX-.js";import"./circle-question-mark-CDYWTUQU.js";const Ce={scraped:{dataset:"contractor",dateField:"scraped_at",label:"Scraped"},enriched:{dataset:"contractor",dateField:"enriched_at",label:"Enriched"},with_email:{dataset:"contractor",dateField:"enriched_at",label:"Has email"},with_narrative:{dataset:"contractor",dateField:"narrative_generated_at",label:"Has narrative"},pending_enrich:{dataset:"contractor",dateField:"scraped_at",label:"Pending enrichment"},failed_enrich:{dataset:"contractor",dateField:"enrichment_last_attempt_at",label:"Failed enrichment"},enrolled:{dataset:"contact",dateField:"created_at",label:"Enrolled"},sent:{dataset:"send",dateField:"sent_at",label:"Sent"},opened:{dataset:"send",dateField:"opened_at",label:"Opened"},clicked:{dataset:"send",dateField:"clicked_at",label:"Clicked"},replied:{dataset:"contact",dateField:"last_replied_at",label:"Replied"}},Se=["id","name","city","state","zip","phone","email","website","tier","approved","rating","reviews","source_url","scraped_at","enriched_at","narrative_generated_at","enrichment_status","enrichment_attempts","enrichment_error","enrichment_last_attempt_at","enrichment_next_retry_at"],ke=["id","email","name","company","phone","city","state","status","audie
1nce","source","source_id","tags","engagement_score","last_emailed_at","last_replied_at","lead_status","created_at"],Fe=["send_id","campaign_id","campaign_name","step_id","contact_id","email","name","company","city","state","status","scheduled_for","sent_at","opened_at","clicked_at","open_count","click_count","error","source","last_replied_at"],Ee={contractor:Se,contact:ke,send:Fe},Ae=S({method:"GET"}).middleware([k]).handler(F("dacc3e7500b9bca40977e55a18660f840b344c106116fe77ebe723f70d973439")),Re=S({method:"POST"}).middleware([k]).handler(F("456d2c36b9e413a58f66428428826d7d93b6d8da17cfb5cc52de1f1fc5ba8957")),Te=S({method:"GET"}).middleware([k]).handler(F("e47fa04dd9cb44f9d6274e220b563d45d8b290a7ab98bc3703781b01fa4a64a8")),Oe=S({method:"POST"}).middleware([k]).handler(F("6ba547fa4040b93c9e92c5c911f060af84915f4412a6c8ba4a0df2481e367adc")),Le=S({method:"GET"}).middleware([k]).handler(F("b6e1011a6d7a82cbc740763c4880522f551d0f111f2c68abea6b29ff6e0c2373")),De=S({method:"GET"}).middleware([k]).handler(F("72f81dbab319194c2e6bcc3cd8363ac553cde5ace8e961cf691a4b0e01e55789")),Me=S({method:"POST"}).middleware([k]).handler(F("70600712a7f03ee19f4afc9925050927be89e36d3680da65e48b7f76275beb22"));function Ge(f,j){const d=new Blob(j,{type:"text/csv;charset=utf-8;"}),n=URL.createObjectURL(d),o=document.createElement("a");o.href=n,o.download=f,document.body.appendChild(o),o.click(),o.remove(),URL.revokeObjectURL(n)}const u="__any__";function Pe({open:f,onOpenChange:j,stage:d}){const n=C(Re),o=C(Te),x=Ce[d],g=r.useMemo(()=>[...Ee[x.dataset]],[x.dataset]),E=x.dataset==="send"||d==="enrolled",[A,b]=r.useState(null),[v,V]=r.useState(""),[H,$]=r.useState(""),[R,q]=r.useState(u),[T,O]=r.useState(u),[z,X]=r.useState(u),[y,w]=r.useState(g),[N,Q]=r.useState(!1),[_,L]=r.useState(0);r.useEffect(()=>{w(g)},[g]),r.useEffect(()=>{if(!f)return;let s=!1;return(async()=>{try{const i=await o();s||b(i)}catch(i){c.error(i?.message??"Failed to load filter options")}})(),()=>{s=!0}},[f,o]);function ee(s,i){w(t=>i?Array.from(new Set([...t,s])):t.filter(a=>a!==s))}async function D(){if(y.length===0){c.error("Pick at least one column");return}Q(!0),L(0);try{const s=m=>m?new Date(m).toISOString():null,i=[];let t=null,a=0,p=`funnel-${d}.csv`;for(;;){const m=await n({data:{stage:d,from:s(v),to:s(H),state:R===u?null:R,city:T===u?null:T,campaign_id:z===u?null:z,columns:y,cursor:t,limit:1e3}});if(p=m.filename,i.length===0&&i.push(m.header,`
2`),m.csv&&i.push(m.csv,`
3`),a+=m.rows,L(a),!m.nextCursor)break;t=m.nextCursor}if(!a){c.message("No rows for these filters");return}Ge(p,i),c.success(`Exported ${a} row${a===1?"":"s"}`),j(!1)}catch(s){c.error(s?.message??"Export failed")}finally{Q(!1)}}return e.jsx(pe,{open:f,onOpenChange:j,children:e.jsxs(ue,{className:"max-w-2xl max-h-[90vh] overflow-y-auto",children:[e.jsxs(fe,{children:[e.jsxs(je,{children:["Export — ",x.label]}),e.jsxs(ge,{children:["Filter and pick the columns you want. Date range applies to"," ",e.jsx("code",{className:"text-xs",children:x.dateField}),"."]})]}),e.jsxs("div",{className:"space-y-5",children:[e.jsxs("div",{className:"grid sm:grid-cols-2 gap-3",children:[e.jsxs("div",{children:[e.jsx(h,{htmlFor:"from",children:"From"}),e.jsx(K,{id:"from",type:"date",value:v,onChange:s=>V(s.target.value)})]}),e.jsxs("div",{children:[e.jsx(h,{htmlFor:"to",children:"To"}),e.jsx(K,{id:"to",type:"date",value:H,onChange:s=>$(s.target.value)})]}),e.jsxs("div",{children:[e.jsx(h,{children:"State"}),e.jsxs(te,{value:R,onValueChange:q,children:[e.jsx(se,{children:e.jsx(ae,{placeholder:"Any"})}),e.jsxs(re,{children:[e.jsx(U,{value:u,children:"Any state"}),(A?.states??[]).map(s=>e.jsx(U,{value:s,children:s},s))]})]})]}),e.jsxs("div",{children:[e.jsx(h,{children:"City"}),e.jsxs(te,{value:T,onValueChange:O,children:[e.jsx(se,{children:e.jsx(ae,{placeholder:"Any"})}),e.jsxs(re,{className:"max-h-72",children:[e.jsx(U,{value:u,children:"Any city"}),(A?.cities??[]).map(s=>e.jsx(U,{value:s,children:s},s))]})]})]}),E&&e.jsxs("div",{className:"sm:col-span-2",children:[e.jsx(h,{children:"Campaign"}),e.jsxs(te,{value:z,onValueChange:X,children:[e.jsx(se,{children:e.jsx(ae,{placeholder:"Any"})}),e.jsxs(re,{children:[e.jsx(U,{value:u,children:"Any campaign"}),(A?.campaigns??[]).map(s=>e.jsxs(U,{value:s.id,children:[s.name," ",e.jsxs("span",{className:"text-xs text-muted-foreground",children:["· ",s.status]})]},s.id))]})]})]})]}),e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between mb-2",children:[e.jsxs(h,{children:["Columns (",y.length,"/",g.length,")"]}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(l,{size:"sm",variant:"ghost",className:"h-7",onClick:()=>w(g),children:"All"}),e.jsx(l,{size:"sm",variant:"ghost",className:"h-7",onClick:()=>w([]),children:"None"})]})]}),e.jsx("div",{className:"grid grid-cols-2 sm:grid-cols-3 gap-x-3 gap-y-2 rounded-md border p-3 max-h-64 overflow-y-auto",children:g.map(s=>{const i=y.includes(s);return e.jsxs("label",{className:"flex items-center gap-2 text-sm cursor-pointer",children:[e.jsx(ye,{checked:i,onCheckedChange:t=>ee(s,!!t)}),e.jsx("span",{className:"truncate",title:s,children:s})]},s)})})]})]}),e.jsxs(be,{children:[e.jsx(l,{variant:"outline",onClick:()=>j(!1),disabled:N,children:"Cancel"}),e.jsxs(l,{onClick:D,disabled:N||y.length===0,children:[N?e.jsx(ve,{className:"h-4 w-4 mr-2 animate-spin"}):e.jsx(J,{className:"h-4 w-4 mr-2"}),N&&_>
30?`Exporting ${_.toLocaleString()}…`:"Export CSV"]})]})]})})}function Xe(){const f=le(),j=ce(f?.user),d=de(j,"superadmin"),[n,o]=r.useState(null),[x,g]=r.useState([]),[E,A]=r.useState(!0),[b,v]=r.useState(5),[V,H]=r.useState(!1),[$,R]=r.useState(null),[q,T]=r.useState(null),[O,z]=r.useState("overview"),X=C(Oe),y=C(Ae),w=C(Le),N=C(De),Q=C(Me),_=r.useCallback(async()=>{H(!0);try{const[t,a,p]=await Promise.all([y(),w(),N()]);o(t),g(a?.rows??[]),A(!!p?.auto_retry),v(Number(p?.max_attempts??5))}catch(t){c.error(t?.message??"Failed to load")}finally{H(!1)}},[N,w,y]);r.useEffect(()=>{d&&_()},[d,_]);function L(t){R(t)}async function ee(t){T(t);try{const a=await X({data:{id:t}});a.ok?c.success("Enrichment retry queued"):c.error(a.error||"Still failing — will retry on schedule"),await _()}catch(a){c.error(a?.message??"Retry failed")}finally{T(null)}}async function D(t){const a={auto_retry:t.auto_retry??E,max_attempts:t.max_attempts??b};A(a.auto_retry),v(a.max_attempts);try{await Q({data:a}),c.success("Saved")}catch(p){c.error(p?.message??"Save failed")}}if(!f||!j)return e.jsx("div",{className:"p-10 text-sm text-muted-foreground",children:"Checking permissions…"});if(!d)return e.jsx("div",{className:"flex min-h-[50vh] items-center justify-center px-4",children:e.jsxs("div",{className:"max-w-sm text-center",children:[e.jsx("div",{className:"mx-auto mb-4 grid h-12 w-12 place-items-center rounded-2xl bg-muted",children:e.jsx(oe,{className:"h-6 w-6 text-muted-foreground"})}),e.jsx("h2",{className:"font-display text-xl font-semibold",children:"Super Admin only"})]})});const s=[{key:"scraped",stage:"scraped",label:"Scraped",help:"Pool contractors pulled from Firecrawl search"},{key:"enriched",stage:"enriched",label:"Enriched",help:"Site scraped + fields extracted"},{key:"withEmail",stage:"with_email",label:"Has email",help:"An email landed during enrichment"},{key:"withNarrative",stage:"with_narrative",label:"Has narrative",help:"AI write-up live on the directory"},{key:"enrolled",stage:"enrolled",label:"Enrolled",help:"Added to the active outreach campaign"},{key:"sent",stage:"sent",label:"Sent",help:"First outreach email sent"},{key:"opened",stage:"opened",label:"Opened",help:"Contact opened an email"},{key:"clicked",stage:"clicked",label:"Clicked",help:"Contact clicked a link"},{key:"replied",stage:"replied",label:"Replied",help:"Contact replied"}],i=n&&n.scraped>0&&n.enriched/Math.max(n.scraped,1)<.5;return e.jsxs("div",{className:"mx-auto max-w-6xl px-6 py-10",children:[e.jsxs("div",{className:"flex items-start justify-between gap-4 mb-8",children:[e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center gap-2 text-xs uppercase tracking-wider text-muted-foreground",children:[e.jsx(me,{className:"h-3.5 w-3.5"})," SplashIQ Platform"]}),e.jsxs("h1",{className:"font-display text-3xl font-bold mt-1 flex items-center gap-2",children:["Growth funnel",e.jsx(ne,{title:"Growth funnel",description:"End-to-end platform funnel from scraped contractors through enrichment and consent-aware external outreach records. Use this to spot where prospects drop off.",tips:["Tabs: Overview = KPIs, Failed enrichment = one-click retry, Retry settings = auto-retry policy.","Click Export… under any stage card to download a CSV."]})]}),e.jsx("p",{className:"text-sm text-muted-foreground mt-1",children:"Global contractor visibility plus SplashIQ HQ outreach records. Cold delivery stays in the approved external platform."})]}),e.jsxs(l,{onClick:_,variant:"outline",disabled:V,children:[e.jsx(we,{className:`h-4 w-4 mr-2 ${V?"animate-spin":""}`})," Refresh"]})]}),i&&e.jsxs("div",{className:"mb-6 rounded-md border border-amber-500/30 bg-amber-50 dark:bg-amber-950/20 p-3 text-sm text-amber-900 dark:text-amber-200",children:["Enrichment is lagging behind scraping. ",n.pendingEnrich," pending,"," ",n.failedEnrich," failed."," ",E?"Auto-retry is on.":"Auto-retry is OFF — turn it on below."]}),e.jsx("div",{className:"flex items-center gap-2 mb-6 border-b border-border",children:[{id:"overview",label:"Overview",icon:he},{id:"failed",label:"Failed enrichment",icon:Ne},{id:"settings",label:"Retry settings",icon:xe}].map(t=>{const a=t.icon,p=O===t.id;return e.jsxs("button",{type:"button",onClick:()=>z(t.id),className:`flex items-center gap-1.5 px-4 py-2 text-sm border-b-2 transition-colors ${p?"border-primary text-foreground font-medium":"border-transparent text-muted-foreground hover:text-foreground"}`,children:[e.jsx(a,{className:"h-4 w-4"}),t.label]},t.id)})}),O==="overview"&&e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 ga
3p-3 mb-8",children:s.map(t=>{const a=n?.[t.key];return e.jsxs(M,{children:[e.jsx(G,{className:"pb-1",children:e.jsx(P,{className:"text-xs uppercase tracking-wider text-muted-foreground",children:t.label})}),e.jsxs(I,{children:[e.jsx("div",{className:"text-3xl font-bold",children:a??"—"}),e.jsx("p",{className:"text-[11px] text-muted-foreground mt-1 mb-2 min-h-[28px]",children:t.help}),e.jsxs(l,{size:"sm",variant:"ghost",className:"h-7 px-2 -ml-2 text-xs",onClick:()=>L(t.stage),disabled:!a,children:[e.jsx(J,{className:"h-3 w-3 mr-1"}),"Export…"]})]})]},String(t.key))})}),e.jsxs("div",{className:"grid sm:grid-cols-2 gap-4 mb-8",children:[e.jsxs(M,{children:[e.jsxs(G,{children:[e.jsx(P,{className:"text-base",children:"Pipeline health"}),e.jsx(B,{children:"What needs attention right now."})]}),e.jsxs(I,{className:"space-y-2 text-sm",children:[e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx("span",{children:"Pending enrichment"}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(Y,{variant:n&&n.pendingEnrich>50?"destructive":"secondary",children:n?.pendingEnrich??"—"}),e.jsx(l,{size:"sm",variant:"ghost",className:"h-7 px-2",onClick:()=>L("pending_enrich"),children:e.jsx(J,{className:"h-3 w-3"})})]})]}),e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx("span",{children:"Failed enrichment"}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(Y,{variant:n&&n.failedEnrich>20?"destructive":"secondary",children:n?.failedEnrich??"—"}),e.jsx(l,{size:"sm",variant:"ghost",className:"h-7 px-2",onClick:()=>L("failed_enrich"),children:e.jsx(J,{className:"h-3 w-3"})})]})]}),e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx("span",{children:"Active campaigns"}),e.jsx(Y,{variant:"secondary",children:n?.activeCampaigns??"—"})]})]})]}),e.jsxs(M,{children:[e.jsxs(G,{children:[e.jsxs(P,{className:"text-base flex items-center gap-2",children:["Auto-retry",e.jsx(ne,{title:"Auto-retry",description:"When enrichment fails, the system automatically re-tries on an exponential backoff schedule (5m → 15m → 45m → 2h → 6h) until Max attempts is reached.",tips:["Turn OFF if you're debugging — failures will stay put.","Max attempts caps how many times a single contractor is re-tried."]})]}),e.jsx(B,{children:"Exponential backoff: 5m → 15m → 45m → 2h → 6h."})]}),e.jsxs(I,{className:"space-y-4 text-sm",children:[e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx(h,{htmlFor:"auto-retry",className:"cursor-pointer",children:"Enable auto-retry"}),e.jsx(ie,{id:"auto-retry",checked:E,onCheckedChange:t=>D({auto_retry:t})})]}),e.jsxs("div",{className:"flex items-center justify-between gap-3",children:[e.jsx(h,{htmlFor:"max-attempts",children:"Max attempts"}),e.jsx(K,{id:"max-attempts",type:"number",min:1,max:20,className:"w-20",value:b,onChange:t=>v(Number(t.target.value||1)),onBlur:()=>D({max_attempts:Math.min(20,Math.max(1,b))})})]}),e.jsx("p",{className:"text-[11px] text-muted-foreground",children:"Cron runs every 5 minutes and re-fires up to 25 due contractors per tick."})]})]})]}),e.jsxs(M,{children:[e.jsxs(G,{children:[e.jsx(P,{className:"text-base",children:"Jump to"}),e.jsx(B,{children:"Tools driving each stage."})]}),e.jsxs(I,{className:"grid sm:grid-cols-2 gap-2",children:[e.jsx(l,{asChild:!0,variant:"outline",className:"justify-between",children:e.jsxs(W,{to:"/admin/contractor-scrape",children:["Scrape ZIPs ",e.jsx(Z,{className:"h-4 w-4"})]})}),e.jsx(l,{asChild:!0,variant:"outline",className:"justify-between",children:e.jsxs(W,{to:"/admin/contractors",children:["Contractor review ",e.jsx(Z,{className:"h-4 w-4"})]})}),e.jsx(l,{asChild:!0,variant:"outline",className:"justify-between",children:e.jsxs(W,{to:"/marketing/lead-gen",children:["Lead Gen campaigns ",e.jsx(Z,{className:"h-4 w-4"})]})}),e.jsx(l,{asChild:!0,variant:"outline",className:"justify-between",children:e.jsxs(W,{to:"/admin/email-deliverability",children:["Deliverability ",e.jsx(Z,{className:"h-4 w-4"})]})})]})]})]}),O==="failed"&&e.jsxs(M,{children:[e.jsxs(G,{children:[e.jsxs(P,{className:"text-base flex items-center gap-2",children:["Failed / partial enrichment",e.jsx(ne,{title:"Failed enrichment",description:"Contractors whose Firecrawl scrape failed or returned only partial data. Click Retry on a row to re-queue it immediately instead of waiting for the next scheduled backoff.",tips:["Look at the Error column to spot patterns (timeouts, 403s, robots blocks).","Attempts X/Y shows how close a row is to giving up."]})]}),e.jsxs(B,{children:["Most recent ",x.length,". Click retry to re-run immediately, or wait for the scheduled backoff."]}
3)]}),e.jsx(I,{children:x.length===0?e.jsx("div",{className:"text-sm text-muted-foreground py-4 text-center",children:"Nothing failing right now."}):e.jsx("div",{className:"overflow-x-auto",children:e.jsxs("table",{className:"w-full text-sm",children:[e.jsx("thead",{className:"text-xs uppercase tracking-wider text-muted-foreground",children:e.jsxs("tr",{className:"border-b",children:[e.jsx("th",{className:"text-left py-2 pr-3",children:"Contractor"}),e.jsx("th",{className:"text-left py-2 pr-3",children:"Status"}),e.jsx("th",{className:"text-left py-2 pr-3",children:"Attempts"}),e.jsx("th",{className:"text-left py-2 pr-3",children:"Next retry"}),e.jsx("th",{className:"text-left py-2 pr-3",children:"Error"}),e.jsx("th",{className:"text-right py-2"})]})}),e.jsx("tbody",{children:x.map(t=>e.jsxs("tr",{className:"border-b last:border-0",children:[e.jsxs("td",{className:"py-2 pr-3",children:[e.jsx("div",{className:"font-medium",children:t.name}),e.jsxs("div",{className:"text-xs text-muted-foreground",children:[t.city?`${t.city}, `:"",t.state]})]}),e.jsx("td",{className:"py-2 pr-3",children:e.jsx(Y,{variant:t.enrichment_status==="failed"?"destructive":"secondary",children:t.enrichment_status})}),e.jsxs("td",{className:"py-2 pr-3",children:[t.enrichment_attempts??0,"/",b]}),e.jsx("td",{className:"py-2 pr-3 text-xs text-muted-foreground",children:t.enrichment_next_retry_at?new Date(t.enrichment_next_retry_at).toLocaleString():"—"}),e.jsx("td",{className:"py-2 pr-3 text-xs text-muted-foreground max-w-[260px] truncate",title:t.enrichment_error??"",children:t.enrichment_error??"—"}),e.jsx("td",{className:"py-2 text-right",children:e.jsxs(l,{size:"sm",variant:"outline",disabled:q===t.id,onClick:()=>ee(t.id),children:[e.jsx(_e,{className:`h-3 w-3 mr-1 ${q===t.id?"animate-spin":""}`}),"Retry"]})})]},t.id))})]})})})]}),O==="settings"&&e.jsxs(M,{children:[e.jsxs(G,{children:[e.jsx(P,{className:"text-base",children:"Auto-retry settings"}),e.jsx(B,{children:"Exponential backoff: 5m → 15m → 45m → 2h → 6h."})]}),e.jsxs(I,{className:"space-y-4 text-sm max-w-md",children:[e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx(h,{htmlFor:"auto-retry-settings",className:"cursor-pointer",children:"Enable auto-retry"}),e.jsx(ie,{id:"auto-retry-settings",checked:E,onCheckedChange:t=>D({auto_retry:t})})]}),e.jsxs("div",{className:"flex items-center justify-between gap-3",children:[e.jsx(h,{htmlFor:"max-attempts-settings",children:"Max attempts"}),e.jsx(K,{id:"max-attempts-settings",type:"number",min:1,max:20,className:"w-20",value:b,onChange:t=>v(Number(t.target.value||1)),onBlur:()=>D({max_attempts:Math.min(20,Math.max(1,b))})})]}),e.jsx("p",{className:"text-[11px] text-muted-foreground",children:"Cron runs every 5 minutes and re-fires up to 25 due contractors per tick."})]})]}),$&&e.jsx(Pe,{open:!!$,onOpenChange:t=>{t||R(null)},stage:$})]})}export{Xe as component};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.