PageSourceSearch

https://smrtbld.ai/js/pages/project-chg.js

js smrtbld.ai collected 2026-10-05 15:10:45 UTC 25,177 bytes, 610 lines download raw bytes

1import { changeRequestsApi } from '../api/change-requests.js';
2import { offlineApi } from '../offline/offline-api.js';
3import { renderTo, loadTemplates, createTemplateLoader } from '../utils/templates.js';
4import { runWithBusyButton } from '../utils/dom.js';
5import { downloadBlob } from '../utils/download.js';
6import { attachDropdownMenu } from '../utils/dropdown-menu.js';
7import { showLogEmailCompose } from '../utils/email-compose.js';
8import { createDesignerTaskDetail } from './designer-task-detail.js';
9import { updateFilterPills as _updateFilterPills } from '../utils/filter-pills.js';
10import { openChgCreateModal } from '../utils/chg-create-modal.js';
11import { DataView } from '../components/DataView.js';
12import './chg-task-modals.js';   // side-effect: registers CHG_IDN / CHG_STS / CHG_QR / CHG_QTO / CHG_OA / CHG_COI / ADD2PROAPP / CHG_REJECT with the task-modal factory
13import { openTaskModal } from '../utils/task-modal-factory.js';
14
15import { subscribeTaskUpdates } from '../realtime/TaskGroupSync.js';
16
17// Page link values that map to the "chg" sub-module tab
18export const CHG_PAGE_LINKS = ['chg'];
19
20let currentProjectId = null;
21let items = [];
22let filteredItems = [];
23let signal = null;
24let dialogAbort = null;
25const ensureTemplates = createTemplateLoader('components/chg-templates');
26let activeStatusFilter = 'all';
27let onNavigateCallback = null;
28
29// CCM data from API
30let listData = null;
31
32// Shared detail view instance (created once, reused)
33let detailView = null;
34let currentContainerId = 'project-chg-container';
35
36// DataView (table/cards toggle) — recreated each time the list shell renders
37let dataView = null;
38
39// Shared click-to-sort on the table headers (DataView.initSortableHeaders) —
40// recreated alongside dataView; th[data-sort-key] attrs live in the template.
41let sorter = null;
42
43// SignalR live list refresh — subscribed to chglist-{projectId} while the list
44// is loaded; disposed on destroy/project switch (re-subscribed by loadItems).
45let listSync = null;
46
47// Export dropdown handle — set by attachEventHandlers after each list render,
48// used by the page-level export click handler below.
49let exportDropdown = null;
50
51// Own AbortController for the export dropdown's listeners (see attachEventHandlers
52// below) — NOT the page `signal`. attachEventHandlers() re-runs on every list
53// re-render (loadItems, every "back to list" from detail), and attachDropdownMenu
54// binds its outside-click/Escape listeners on `document` — the most persistent
55// target there is. The page signal only aborts on full contract-admin teardown,
56// not on list re-renders, so reusing it here would stack a fresh pair of
57// document-level listeners on every render, forever (each closing over that
58// render's now-detached menu/button — harmless no-ops, but never removed).
59// Same fix as project-rfi.js exportDropdownAbort.
60let exportDropdownAbort = null;
61
62// "Show Deleted" toggle (legacy parity) — includes soft-deleted rows in the
63// list. Per-visit only: reset on destroy/project switch, never persisted.
64let showDeleted = false;
65
66/**
67 * Initialize CHG sub-module.
68 * Called when the CHG tab is selected in Projects.
69 * @param {number} projectId - The selected project ID
70 * @param {AbortSignal} [abortSignal] - AbortController signal for cleanup
71 * @param {Object} [options] - Optional config
72 * @param {string} [options.initialTaskId] - Task ID to deep-link into detail view
73 * @param {Function} [options.onNavigate] - Callback when view changes (for URL updates)
74 */
75export async function initProjectChg(projectId, abortSignal, options = {}) {
76    dialogAbort?.abort();
77    detailView?.destroy();
78    const visitController = new AbortController();
79    dialogAbort = visitController;
80    if (abortSignal?.aborted) visitController.abort();
81    else abortSignal?.addEventListener('abort', () => visitController.abort(), { once: true, signal: visitController.signal });
82    if (visitController.signal.aborted) return;
83    signal = abortSignal || null;
84    onNavigateCallback = options.onNavigate || null;
85    const containerId = options.containerId || 'project-chg-container';
86
87    await ensureTemplates();
88    if (dialogAbort !== visitController || visitController.signal.aborted) return;
89
90    // Recreate detail view if container changed
91    if (currentContainerId !== containerId && detailView) {
92        detailView.destroy();
93        detailView = null;
94    }
95    currentContainerId = containerId;
96
97    // Create detail view instance if not exists
98    if (!detailView) {
99        detailView = createDesignerTaskDetail({
100            api: changeRequestsApi,
101            containerId: currentContainerId,
102            entityLabel: 'CHG',
103            entityIcon: 'git-pull-request',
104            onSpecialAction: handleSpecialAction,
105            itemType: 'CHG',
106            // CA-017 opt-in: issue-package milestone codes (modern + legacy aliases) —
107            // renders the truthful Not sent / Queued / Failed / Sent strip + retry.
108            deliveryStateCodes: ['CHG_STS', 'CISSPK', 'S2SUB'],
109            // Reject dispatcher (audit R4, guard fixed CA-056): the CHG_REJECT modal +
110            // endpoint existed with no way to reach them. Header action (legacy put
111            // "Cancel" in the REAPP row kebab; a header button is the canon home).
112            // Reject is a header action with NO task date, so it dispatches with
113            // taskDateId = null — handleSpecialAction's taskDateId guard is scoped to
114            // the date-type actions only (see there). Server enforces the
115            // rejected-state lock, so re-clicking on a rejected CHG is a harmless re-stamp.
116            extraActions: [{
117                id: 'chg-reject',
118                label: 'Reject',
119                icon: 'ban',
120                onClick: ({ taskId }) => handleSpecialAction('chg-reject', null, null, taskId),
121            }],
122        });
123    }
124
125    // If project changed, clear cached data and reset detail view
126    if (currentProjectId !== projectId) {
127        items = [];
128        filteredItems = [];
129        listData = null;
130        activeStatusFilter = 'all';
131        showDeleted = false;
132        detailView.destroy();
133    }
134    currentProjectId = projectId;
135
136    attachPageLevelHandlers();
137
138    await loadItems();
139    if (dialogAbort !== visitController || visitController.signal.aborted) return;
140
141    // Deep-link into detail view if task ID provided via URL
142    if (options.initialTaskId) {
143        await detailView.loadDetail(currentProjectId, options.initialTaskId, signal, {
144            onNavigate: onNavigateCallback,
145            onBackToList: async () => { await renderList(); attachEventHandlers(); },
146            onReloadList: () => loadItems(),
147        });
148    }
149}
150
151/**
152 * Clean up CHG state.
153 */
154export function destroyProjectChg() {
155    dialogAbort?.abort();
156    dialogAbort = null;
157    detailView?.destroy();
158    detailView = null;
159    dataView?.destroy();
160    dataView = null;
161    sorter?.destroy();
162    sorter = null;
163    listSync?.dispose();
164    listSync = null;
165    currentProjectId = null;
166    items = [];
167    filteredItems = [];
168    listData = null;
169    signal = null;
170    activeStatusFilter = 'all';
171    showDeleted = false;
172    onNavigateCallback = null;
173    currentContainerId = 'project-chg-container';
174    exportDropdownAbort?.abort();
175    exportDropdownAbort = null;
176    exportDropdown = null;
177}
178
179/**
180 * Get the currently viewed CHG task ID (for URL building).
181 */
182export function getCurrentChgTaskId() {
183    return detailView?.getCurrentTaskId() || null;
184}
185
186// ========================================
187// DATA LOADING
188// ========================================
189
190/**
191 * Silent list refresh for SignalR pings — refetch + re-filter without
192 * tearing down the list shell (no loading skeleton, no handler rebinds).
193 */
194async function refreshListData() {
195    if (!currentProjectId) return;
196    const pid = currentProjectId;
197    try {
198        const response = await changeRequestsApi.getList(pid, null, showDeleted);
199        const serverRows = await offlineApi.prioritizeOfflineAvailable(response.rfis || [], {
200            feature: 'change-requests',
201            idGetter: item => item.crId ?? item.rfiId ?? item.taskId,
202        });
203        if (pid !== currentProjectId) return;   // project switched mid-flight — drop stale rows
204        listData = response;
205        items = serverRows;
206        const searchInput = document.getElementById('chg-search');
207        applyFilters(searchInput?.value || '');
208    } catch { /* silent — SignalR will ping again on the next change */ }
209}
210
211async function loadItems(isCurrent = null) {
212    if (isCurrent && !isCurrent()) return;
213    const projectId = currentProjectId;
214    const readSignal = isCurrent ? dialogAbort?.signal : undefined;
215    const container = document.getElementById(currentContainerId);
216    if (!container || !currentProjectId) return;
217
218    // Live list refresh: anyone's create/edit/delete/date/comment on this project's
219    // Change Requests pings chglist-{projectId} over SignalR (the app's ONE transport —
220    // no client polling, ever) → silent row refresh.
221    listSync?.dispose();
222    listSync = subscribeTaskUpdates(`chglist-${currentProjectId}`, () => { refreshListData(); });
223
224    await renderChgOwned(container, '#tpl-chg-loading', {}, isCurrent);
225    if (isCurrent && !isCurrent()) return;
226
227    try {
228        const response = await changeRequestsApi.getList(projectId, null, showDeleted, readSignal ? { signal: readSignal }
228 : undefined);
229        if (isCurrent && !isCurrent()) return;
230        const availableItems = await offlineApi.prioritizeOfflineAvailable(response.rfis || [], {
231            feature: 'change-requests',
232            idGetter: item => item.crId ?? item.rfiId ?? item.taskId,
233        });
234        if (isCurrent && !isCurrent()) return;
235        listData = response;
236        items = availableItems;
237        filteredItems = [...items];
238
239        await renderList(isCurrent);
240        if (isCurrent && !isCurrent()) return;
241        attachEventHandlers();
242    } catch (error) {
243        if ((isCurrent && !isCurrent()) || error.name === 'AbortError') return;
244        window.showToast?.('Failed to load Change Requests', 'error', { description: error.message });
245    }
246}
247
248// ========================================
249// RENDERING
250// ========================================
251
252async function renderChgOwned(container, templateId, data, isCurrent) {
253    if (!isCurrent) return renderTo(container, templateId, data);
254    if (!isCurrent()) return;
255    const staging = document.createElement('div');
256    try { await renderTo(staging, templateId, data); }
257    catch (error) { if (!isCurrent()) return; throw error; }
258    if (isCurrent()) container.replaceChildren(...staging.childNodes);
259}
260
261async function renderList(isCurrent = null) {
262    if (isCurrent && !isCurrent()) return;
263    const container = document.getElementById(currentContainerId);
264    if (!container) return;
265
266    await renderChgOwned(container, '#tpl-chg-list', {
267        totalCount: items.length,
268        overdueCount: listData?.overdueCount || 0,
269        dueTodayCount: listData?.dueTodayCount || 0,
270        upcomingCount: listData?.upcomingCount || 0,
271        closedCount: listData?.closedCount || 0,
272        showDeleted,
273    }, isCurrent);
274    if (isCurrent && !isCurrent()) return;
275
276    // Wire DataView for the table/cards toggle (must happen AFTER the shell
277    // renders so the table-view / cards-view / toggle containers exist).
278    dataView?.destroy();
279    dataView = DataView.init({
280        storageKey: 'chg',
281        tableContainerId: 'chg-table-view',
282        cardsContainerId: 'chg-cards-view',
283        toggleContainerId: 'chg-view-toggle',
284        defaultView: 'table',
285        onViewChange: () => renderCurrentView(isCurrent),
286    });
287
288    sorter?.destroy();
289    sorter = DataView.initSortableHeaders({
290        storageKey: 'chg',
291        tableContainerId: 'chg-table-view',
292        onSort: () => renderCurrentView(isCurrent),
293    });
294
295    await renderCurrentView(isCurrent);
296    if (isCurrent && !isCurrent()) return;
297
298    // Restore active filter pill state
299    updateFilterPills();
300}
301
302/**
303 * Render the active view (table or cards) from filteredItems.
304 */
305async function renderCurrentView(isCurrent = null) {
306    if (isCurrent && !isCurrent()) return;
307    const tableBody = document.getElementById('chg-table-body');
308    const cardsContainer = document.getElementById('chg-items');
309
310    if (filteredItems.length === 0) {
311        if (tableBody) await renderChgOwned(tableBody, '#tpl-chg-table-empty', {}, isCurrent);
312        if (cardsContainer) await renderChgOwned(cardsContainer, '#tpl-chg-empty', {}, isCurrent);
313        return;
314    }
315
316    // Header sort applies to BOTH views so toggling table↔cards keeps the order
317    const rows = sorter ? sorter.sortItems(filteredItems) : filteredItems;
318    const view = dataView?.getView() ?? 'table';
319    if (view === 'table' && tableBody) {
320        await renderChgOwned(tableBody, '#tpl-chg-table-row', rows, isCurrent);
321    } else if (cardsContainer) {
322        await renderChgOwned(cardsContainer, '#tpl-chg-row', rows, isCurrent);
323    }
324}
325
326function updateFilterPills() {
327    _updateFilterPills('.chg-filter-pill', activeStatusFilter);
328}
329
330function attachEventHandlers() {
331    const container = document.getElementById(currentContainerId);
332    if (!container) return;
333
334    const opts = signal ? { signal } : {};
335
336    // Search
337    const searchInput = document.getElementById('chg-search');
338    searchInput?.addEventListener('input', (e) => {
339        applyFilters(e.target.value);
340    }, opts);
341
342    // Status filter pills
343    const filterContainer = document.getElementById('chg-status-filters');
344    filterContainer?.addEventListener('click', (e) => {
345        const pill = e.target.closest('.chg-filter-pill');
346        if (!pill) return;
347        activeStatusFilter = pill.dataset.filter;
348        const searchInput = document.getElementById('chg-search');
349        applyFilters(searchInput?.value || '');
350        updateFilterPills();
351    }, opts);
352
353    // "Show Deleted" toggle — server-side param, so flip → full reload. The
354    // shell re-render restores the checked state from module-level `showDeleted`.
355    const showDeletedToggle = document.getElementById('chg-show-deleted');
356    showDeletedToggle?.addEventListener('change', () => {
357        showDeleted = showDeletedToggle.checked;
358        loadItems();
359    }, opts);
360
361    // ── Export dropdown toggle (button is inside the re-rendered list shell) ──
362    // Abort the PREVIOUS render's dropdown listeners before attaching a fresh pair
363    // (see exportDropdownAbort declaration above) — otherwise document click/keydown
364    // listeners stack forever across list<->detail round trips.
365    exportDropdownAbort?.abort();
366    exportDropdownAbort = new AbortController();
367    exportDropdown = attachDropdownMenu('btn-chg-export', 'chg-export-menu', { signal: exportDropdownAbort.signal });
368}
369
370// Bound once per page init (not per list render) — these delegate on the
371// persistent outer container element and must not stack across re-renders.
372// Same hazard + same cure as project-rfi.js attachPageLevelHandlers: the page
373// AbortSignal only aborts on full page teardown, not on sub-module/project
374// switch, so re-binding on every render STACKS listeners (one click on "New"
375// was opening 4-5 identical create modals). Handlers read module-level state
376// (currentProjectId, detailView, signal, …) at fire time, so bind-once is safe.
377function attachPageLevelHandlers() {
378    const container = document.getElementById(currentContainerId);
379    if (!container) return;
380
381    if (container.dataset.chgPageHandlersBound === 'true') return;
382    container.dataset.chgPageHandlersBound = 'true';
383
384    const opts = signal ? { signal } : {};
385
386    // New Change Request button (delegated — works from header button AND empty state CTA)
387    container.addEventListener('click', (e) => {
388        if (e.target.closest('[data-action="new-chg"]')) {
389            showAddModal();
390        }
391    }, opts);
392
393    // "Send To" button (delegated — emails the Change Log; legacy Send To)
394    container.addEventListener('click', (e) => {
395        if (e.target.closest('[data-action="send-chg-log"]')) {
396            showSendLogModal();
397        }
398    }, opts);
399
400    // ── Export button clicks (legacy Create Report PDF / XLS) ──
401    container.addEventListener('click', async (e) => {
402        const btn = e.target.closest('.chg-export-btn');
403        if (!btn) return;
404        exportDropdown?.close();
405
406        const format = btn.dataset.format;
407        // Legacy report change-type filters: All (omit) / BOTH (Owner Initiated +
408        // Charge to Client) / OWNI / CHCL / BKCH / SCVI.
409        const changeType = btn.dataset.changeType || null;
410
411        await runWithBusyButton(btn, 'Generating...', async () => {
412            const { blob, fileName } = format === 'pdf'
413                ? await changeRequestsApi.downloadLogPdf(currentProjectId, null, null, changeType)
414                : await changeRequestsApi.downloadLogExcel(currentProjectId, null, null, changeType);
415
416            downloadBlob(blob, fileName, { successMessage: `${format.toUpperCase()} downloaded` });
417        }, { errorTitle: 'Export failed', errorLogPrefix: '[ProjectChg] Export failed:', resetOnSuccess: true });
418    }, opts);
419
420    // Row click — navigate to detail view. Delegated from `container` so it
421    // fires for both card rows (inside #chg-items) and table rows (inside
422    // #chg-table-body) — both carry `data-chg-id` on the row element.
423    container.addEventListener('click', (e) => {
424        const row = e.target.closest('[data-chg-id]');
425        if (!row) return;
426        if (row.dataset.offlineUnavailable === 'true') {
427            window.showToast?.('Not saved for offline', 'info', {
428                description: 'Open this Change Request while online to save it for offline viewing.',
429            });
430            return;
431        }
432        detailView.loadDetail(currentProjectId, row.dataset.chgId, signal, {
433            onNavigate: onNavigateCallback,
434            onBackToList: async () => { await renderList(); attachEventHandlers(); },
435            onReloadList: () => loadItems(),
436        });
437    }, opts);
438}
439
440// ========================================
441// FILTERING
442// ========================================
443
444function applyFilters(searchTerm = '') {
445    const term = searchTerm.toLowerCase();
446
447    filteredItems = items.filter(r => {
448        // Status filter
449        if (activeStatusFilter !== 'all' && r.status !== activeStatusFilter) {
450            return false;
451        }
452
453        // Text search
454        if (term) {
455            const title = (r.title || '').toLowerCase();
456            const ref = (r.referenceNumber || '').toLowerCase();
457            const extraRef = (r.extraReferenceNumber || '').toLowerCase();
458            const assigned = (r.assigned || '').toLowerCase();
459            const changeType = (r.changeTypeDesc || '').toLowerCase();
460            return title.includes(term) || ref.includes(term) || extraRef.includes(term) || assigned.includes(term) || changeType.includes(term);
461        }
462
463        return true;
464    });
465
466    renderFilteredRows();
467}
468
469async function renderFilteredRows() {
470    await renderCurrentView();
471    const countEl = document.getElementById('chg-count');
472    if (countEl) countEl.textContent = filteredItems.length;
473}
474
475// ========================================
476// SPECIAL ACTION MODALS (CHG Date Types)
477// ========================================
478
479async function handleSpecialAction(action, taskDateId, dateTypeId, taskId) {
480    const actionController = dialogAbort;
481    const dialogSignal = actionController?.signal;
482    const projectId = currentProjectId;
483    const ownedDetail = detailView;
484    const ownedNavigate = onNavigateCallback;
485    const isCurrentVisit = () => dialogAbort === actionController && !dialogSignal?.aborted
486        && currentProjectId === projectId && detailView === ownedDetail;
487    const isCurrentAction = () => isCurrentVisit() && String(ownedDetail?.getCurrentTaskId()) === String(taskId);
488    const isCurrentCallback = () => isCurrentVisit()
489        && (ownedDetail?.getCurrentTaskId() == null || String(ownedDetail.getCurrentTaskId()) === String(taskId));
490    if (!projectId || !actionController || !taskId || !isCurrentAction()) return;
491    // Header Reject has no task date (MCF-0143).
492    if (action !== 'chg-reject' && !taskDateId) return;
493
494    try {
495        const onComplete = async () => {
496            if (!isCurrentAction()) return;
497            try {
498                await ownedDetail.loadDetail(projectId, taskId, dialogSignal, {
499                    onNavigate: (...args) => { if (isCurrentCallback()) ownedNavigate?.(...args); },
500                    onBackToList: async () => {
501                        if (!isCurrentCallback()) return;
502                        await renderList(isCurrentCallback);
503                        if (isCurrentCallback()) attachEventHandlers();
504                    },
505                    onReloadList: () => { if (isCurrentCallback()) return loadItems(isCurrentCallback); },
506                });
507            } catch (error) {
508                if (!isCurrentAction() || error.name === 'AbortError') return;
509                window.showToast?.('Failed to open form', 'error', { description: error.message });
510            }
511        };
512
513        // Reject needs neither member nor cost-code options.
514        if (action === 'chg-reject') {
515            await openTaskModal('CHG_REJECT', { projectId, taskId, api: changeRequestsApi, signal: dialogSignal, onComplete });
516            return;
517        }
518
519        const opts = await changeRequestsApi.getAddOptions(projectId, { signal: dialogSignal });
520        if (!isCurrentAction()) return;
521        const members = opts.members || [];
522
523        // Fetch cost codes for Add to Budget modal (degrades gracefully if endpoint missing)
524        let costCodes = [];
525        try { costCodes = await changeRequestsApi.getCostCodes?.(projectId, { signal: dialogSignal }) || []; }
526        catch (error) { if (!isCurrentAction() || error.name === 'AbortError') return; }
527        if (!isCurrentAction()) return;
528
529        const commonOpts = {
530            projectId,
531            taskId,
532            taskDateId,
533            dateTypeId,
534            api: changeRequestsApi,
535            members,
536            costCodes,
537            signal: dialogSignal,
538            onComplete,
539        };
540
541        const codeByAction = {
542            'chg-identified':     'CHG_IDN',
543            'chg-sent-sub':       'CHG_STS',
544            'chg-quote-received': 'CHG_QR',
545            'chg-quote-to-owner': 'CHG_QTO',
546            'chg-owner-approval': 'CHG_OA',
547            'chg-co-issued':      'CHG_COI',
548            'chg-add-to-budget':  'ADD2PROAPP',
549        };
550        const code = codeByAction[action];
551        if (code) await openTaskModal(code, commonOpts);
552    } catch (error) {
553        if (!isCurrentAction() || error.name === 'AbortError') return;
554        window.showToast?.('Failed to open form', 'error', { description: error.message });
555    }
556}
557
558// ========================================
559// SEND LOG ("Send To" — email the Change Log)
560// ========================================
561
562/**
563 * Open the shared compose modal to email the Change Log PDF to picked recipients.
564 * Legacy parity: "Send To" (email the change log). Legacy sends the FULL log,
565 * so no filter scoping here — the PDF is generated server-side.
566 * Same pattern as project-rfi.js showSendLogModal.
567 */
568async function showSendLogModal() {
569    if (!currentProjectId) return;
570
571    const proj = listData?.project || {};
572    // Legacy subject shape: "MMM dd, yyyy - CHANGE LOG | {ProjectNumber} | {ProjectTitle}".
573    const dateStr = new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });
574    const parts = [`${dateStr} - CHANGE LOG`];
575    if (proj.projectNumber) parts.push(String(proj.projectNumber));
576    if (proj.projectTitle) parts.push(proj.projectTitle);
577    const subject = parts.join(' | ');
578    const subtitle = [proj.projectNumber, proj.projectTitle].filter(Boolean).join(' — ');
579
580    await showLogEmailCompose({
581        projectId: currentProjectId,
582        entityLabel: 'Change Log',
583        subject,
584        subtitle,
585        sendFn: (payload) => changeRequestsApi.sendLogEmail(currentProjectId, payload),
586    });
587}
588
589// ========================================
590// CREATE MODAL
591// ========================================
592
593async function showAddModal() {
594    if (!currentProjectId) return;
595    await openChgCreateModal({
596        projectId: currentProjectId,
597        signal,
598        onCreated: async ({ chgId, title }) => {
599            window.showToast?.('Proposed Change created', 'success', { description: title });
600            await loadItems();
601            if (chgId && detailView) {
602                detailView.loadDetail(currentProjectId, chgId, signal, {
603                    onNavigate: onNavigateCallback,
604                    onBackToList: async () => { await renderList(); attachEventHandlers(); },
605                    onReloadList: () => loadItems(),
606                });
607            }
608        },
609    });
610}

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.