Now let's do a comprehensive mobile rewrite of the frontend. The main mobile issues are:
- Actions row overflow
- Filter wrapping
- Permit card meta items
- Status-row labels getting clipped
- Detail sections overflow
- Overall touch target sizes

    /* Action Buttons */
    .actions {
      display: flex;
      gap: 10px;
      margin-bottom: 20px;
      flex-wrap: wrap;
    }

    .btn {
      padding: 12px 20px;
      border: none;
      border-radius: var(--radius-sm);
      cursor: pointer;
      font-size: 0.9rem;
      font-weight: 600;
      transition: all 0.2s;
      display: inline-flex;
      align-items: center;
      gap: 8px;
    }

    .btn-primary {
      background: linear-gradient(135deg, var(--accent-primary) 0%, var(--accent-secondary) 100%);
      color: #fff;
    }

    .btn-primary:hover {
      transform: translateY(-1px);
      box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
    }

    .btn-secondary {
      background: var(--bg-tertiary);
      color: var(--text-primary);
      border: 1px solid var(--border);
    }

    .btn-secondary:hover {
      background: var(--bg-card);
    }

    .btn-success {
      background: linear-gradient(135deg, var(--success) 0%, #059669 100%);
      color: #fff;
    }

    .btn-success:hover {
      transform: translateY(-1px);
      box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
    }

    /* Action Buttons */
    .actions {
      display: flex;
      gap: 8px;
      margin-bottom: 20px;
      flex-wrap: wrap;
      align-items: center;
    }

    .btn {
      padding: 10px 14px;
      border: none;
      border-radius: var(--radius-sm);
      cursor: pointer;
      font-size: 0.85rem;
      font-weight: 600;
      transition: all 0.2s;
      display: inline-flex;
      align-items: center;
      gap: 6px;
      white-space: nowrap;
    }

    .btn-primary {
      background: linear-gradient(135deg, var(--accent-primary) 0%, var(--accent-secondary) 100%);
      color: #fff;
    }

    .btn-primary:hover {
      transform: translateY(-1px);
      box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
    }

    .btn-secondary {
      background: var(--bg-tertiary);
      color: var(--text-primary);
      border: 1px solid var(--border);
    }

    .btn-secondary:hover {
      background: var(--bg-card);
    }

    .btn-success {
      background: linear-gradient(135deg, var(--success) 0%, #059669 100%);
      color: #fff;
    }

    .btn-success:hover {
      transform: translateY(-1px);
      box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
    }


Successfully replaced text in /home/crogers2287/accela/apps/permit-tracker/public/index.html.

      ...
   95
   96     /* Action Buttons */
   97     .actions {
   98       display: flex;
-  99       gap: 10px;
+  99       gap: 8px;
  100       margin-bottom: 20px;
  101       flex-wrap: wrap;
+ 102       align-items: center;
  102     }
  103
  104     .btn {
- 105       padding: 12px 20px;
+ 106       padding: 10px 14px;
  106       border: none;
  107       border-radius: var(--radius-sm);
  108       cursor: pointer;
- 109       font-size: 0.9rem;
+ 110       font-size: 0.85rem;
  110       font-weight: 600;
  111       transition: all 0.2s;
  112       display: inline-flex;
  113       align-items: center;
- 114       gap: 8px;
+ 115       gap: 6px;
+ 116       white-space: nowrap;
  115     }
  116
  117     .btn-primary {
  118       background: linear-gradient(135deg, var(--accent-primary) 0%, var(--accent-secondary) 100%);
      ...


edit

    /* Status Filter Dropdown */
    .filter-group {
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .filter-select {
      padding: 10px 14px;
      border-radius: var(--radius-sm);
      border: 1px solid var(--border);
      background: var(--bg-tertiary);
      color: var(--text-primary);
      font-size: 0.9rem;
      cursor: pointer;
      min-width: 150px;
    }

    .filter-select:focus {
      outline: none;
      border-color: var(--accent-primary);
    }

    .filter-label {
      color: var(--text-secondary);
      font-size: 0.85rem;
    }

    /* Status Filter Dropdown */
    .filter-group {
      display: flex;
      align-items: center;
      gap: 6px;
      margin-left: auto;
    }

    .filter-select {
      padding: 10px 10px;
      border-radius: var(--radius-sm);
      border: 1px solid var(--border);
      background: var(--bg-tertiary);
      color: var(--text-primary);
      font-size: 0.85rem;
      cursor: pointer;
      min-width: 130px;
      max-width: 160px;
    }

    .filter-select:focus {
      outline: none;
      border-color: var(--accent-primary);
    }

    .filter-label {
      color: var(--text-secondary);
      font-size: 0.8rem;
      white-space: nowrap;
    }


      ...
  366     /* Status Filter Dropdown */
  367     .filter-group {
  368       display: flex;
  369       align-items: center;
- 370       gap: 8px;
+ 370       gap: 6px;
+ 371       margin-left: auto;
  371     }
  372
  373     .filter-select {
- 374       padding: 10px 14px;
+ 375       padding: 10px 10px;
  375       border-radius: var(--radius-sm);
  376       border: 1px solid var(--border);
  377       background: var(--bg-tertiary);
  378       color: var(--text-primary);
- 379       font-size: 0.9rem;
+ 380       font-size: 0.85rem;
  380       cursor: pointer;
- 381       min-width: 150px;
+ 382       min-width: 130px;
+ 383       max-width: 160px;
  382     }
  383
  384     .filter-select:focus {
  385       outline: none;
  386       border-color: var(--accent-primary);
  387     }
  388
  389     .filter-label {
  390       color: var(--text-secondary);
- 391       font-size: 0.85rem;
+ 393       font-size: 0.8rem;
+ 394       white-space: nowrap;
  392     }
  393
  394     /* Hide completed/withdrawn when filtered */
  395     .permit-card.status-hidden {
      ...


Now fix the permit meta row to handle small screens better, and the permit-top:

    .permit-top {
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
      gap: 12px;
      margin-bottom: 8px;
    }

    .permit-number {
      font-weight: 600;
      color: var(--accent-secondary);
      font-size: 0.95rem;
    }

    .permit-number a {
      color: inherit;
      text-decoration: none;
    }

    .permit-number a:hover {
      text-decoration: underline;
    }

    .status-badge {
      display: inline-block;
      padding: 4px 10px;
      border-radius: 12px;
      font-size: 0.7rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.3px;
      white-space: nowrap;
    }

    .permit-top {
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
      gap: 8px;
      margin-bottom: 6px;
      flex-wrap: nowrap;
    }

    .permit-number {
      font-weight: 600;
      color: var(--accent-secondary);
      font-size: 0.9rem;
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .permit-number a {
      color: inherit;
      text-decoration: none;
    }

    .permit-number a:hover {
      text-decoration: underline;
    }

    .status-badge {
      display: inline-block;
      padding: 3px 8px;
      border-radius: 12px;
      font-size: 0.65rem;
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: 0.3px;
      white-space: nowrap;
      flex-shrink: 0;
    }


      ...
  402     .permit-top {
  403       display: flex;
  404       justify-content: space-between;
  405       align-items: flex-start;
- 406       gap: 12px;
- 407       margin-bottom: 8px;
+ 406       gap: 8px;
+ 407       margin-bottom: 6px;
+ 408       flex-wrap: nowrap;
  408     }
  409
  410     .permit-number {
  411       font-weight: 600;
  412       color: var(--accent-secondary);
- 413       font-size: 0.95rem;
+ 414       font-size: 0.9rem;
+ 415       min-width: 0;
+ 416       overflow: hidden;
+ 417       text-overflow: ellipsis;
+ 418       white-space: nowrap;
  414     }
  415
  416     .permit-number a {
  417       color: inherit;
  418       text-decoration: none;
  419     }
  420
  421     .permit-number a:hover {
  422       text-decoration: underline;
  423     }
  424
  425     .status-badge {
  426       display: inline-block;
- 427       padding: 4px 10px;
+ 432       padding: 3px 8px;
  428       border-radius: 12px;
- 429       font-size: 0.7rem;
- 430       font-weight: 600;
+ 434       font-size: 0.65rem;
+ 435       font-weight: 700;
  431       text-transform: uppercase;
  432       letter-spacing: 0.3px;
  433       white-space: nowrap;
+ 439       flex-shrink: 0;
  434     }
  435
  436     .status-badge.awaiting { background: var(--danger); color: #fff; }
  437     .status-badge.issued { background: var(--success); color: #fff; }
      ...


    .permit-type {
      font-size: 0.85rem;
      color: var(--text-secondary);
      margin-bottom: 6px;
    }

    .permit-address {
      font-size: 0.9rem;
      color: var(--text-primary);
    }

    .permit-description {
      font-size: 0.85rem;
      color: var(--text-muted);
      margin-top: 4px;
      padding: 6px 10px;
      background: var(--bg-secondary);
      border-radius: 4px;
      border-left: 3px solid var(--primary);
      font-style: italic;
    }

    .permit-meta {
      display: flex;
      flex-wrap: wrap;
      gap: 16px;
      margin-top: 12px;
      padding-top: 12px;
      border-top: 1px solid var(--border);
    }

    .meta-item {
      display: flex;
      align-items: center;
      gap: 6px;
      font-size: 0.8rem;
    }

    .meta-icon {
      width: 16px;
      height: 16px;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 0.75rem;
    }

    .meta-item.has-issues { color: var(--danger); font-weight: 600; }
    .meta-item.has-noc { color: var(--success); }
    .meta-item.no-noc { color: var(--warning); }
    .meta-item.has-pp { color: var(--info); }

    .permit-type {
      font-size: 0.8rem;
      color: var(--text-secondary);
      margin-bottom: 4px;
    }

    .permit-address {
      font-size: 0.875rem;
      color: var(--text-primary);
      word-break: break-word;
    }

    .permit-description {
      font-size: 0.8rem;
      color: var(--text-muted);
      margin-top: 4px;
      padding: 5px 8px;
      background: var(--bg-secondary);
      border-radius: 4px;
      border-left: 3px solid var(--accent-primary);
      font-style: italic;
      word-break: break-word;
    }

    .permit-meta {
      display: flex;
      flex-wrap: wrap;
      gap: 8px 14px;
      margin-top: 10px;
      padding-top: 10px;
      border-top: 1px solid var(--border);
    }

    .meta-item {
      display: flex;
      align-items: center;
      gap: 4px;
      font-size: 0.75rem;
    }

    .meta-icon {
      width: 14px;
      height: 14px;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 0.7rem;
    }

    .meta-item.has-issues { color: var(--danger); font-weight: 600; }
    .meta-item.has-noc { color: var(--success); }
    .meta-item.no-noc { color: var(--warning); }
    .meta-item.has-pp { color: var(--info); }


      ...
  446     .status-badge.withdrawn { background: var(--text-muted); color: #fff; }
  447     .status-badge.process { background: var(--info); color: #fff; }
  448
  449     .permit-type {
- 450       font-size: 0.85rem;
+ 450       font-size: 0.8rem;
  451       color: var(--text-secondary);
- 452       margin-bottom: 6px;
+ 452       margin-bottom: 4px;
  453     }
  454
  455     .permit-address {
- 456       font-size: 0.9rem;
+ 456       font-size: 0.875rem;
  457       color: var(--text-primary);
+ 458       word-break: break-word;
  458     }
  459
  460     .permit-description {
- 461       font-size: 0.85rem;
+ 462       font-size: 0.8rem;
  462       color: var(--text-muted);
  463       margin-top: 4px;
- 464       padding: 6px 10px;
+ 465       padding: 5px 8px;
  465       background: var(--bg-secondary);
  466       border-radius: 4px;
- 467       border-left: 3px solid var(--primary);
+ 468       border-left: 3px solid var(--accent-primary);
  468       font-style: italic;
+ 470       word-break: break-word;
  469     }
  470
  471     .permit-meta {
  472       display: flex;
  473       flex-wrap: wrap;
- 474       gap: 16px;
- 475       margin-top: 12px;
- 476       padding-top: 12px;
+ 476       gap: 8px 14px;
+ 477       margin-top: 10px;
+ 478       padding-top: 10px;
  477       border-top: 1px solid var(--border);
  478     }
  479
  480     .meta-item {
  481       display: flex;
  482       align-items: center;
- 483       gap: 6px;
- 484       font-size: 0.8rem;
+ 485       gap: 4px;
+ 486       font-size: 0.75rem;
  485     }
  486
  487     .meta-icon {
- 488       width: 16px;
- 489       height: 16px;
+ 490       width: 14px;
+ 491       height: 14px;
  490       display: flex;
  491       align-items: center;
  492       justify-content: center;
- 493       font-size: 0.75rem;
+ 495       font-size: 0.7rem;
  494     }
  495
  496     .meta-item.has-issues { color: var(--danger); font-weight: 600; }
  497     .meta-item.has-noc { color: var(--success); }
      ...


Now fix the `status-row` in the detail sections to handle long labels on mobile:

    /* Private Provider & NOC Status */
    .status-row {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 12px;
      background: var(--bg-secondary);
      border-radius: var(--radius-sm);
      margin-bottom: 8px;
    }

    .status-label {
      font-size: 0.85rem;
      color: var(--text-secondary);
    }

    .status-value {
      font-weight: 600;
      font-size: 0.85rem;
      padding: 4px 12px;
      border-radius: 6px;
    }

    .status-value.success {
      background: rgba(16, 185, 129, 0.2);
      color: var(--success);
    }

    .status-value.warning {
      background: rgba(245, 158, 11, 0.2);
      color: var(--warning);
    }

    .status-value.info {
      background: rgba(59, 130, 246, 0.2);
      color: var(--info);
    }

    .status-value.danger {
      background: rgba(239, 68, 68, 0.2);
      color: var(--danger);
    }

    /* Private Provider & NOC Status */
    .status-row {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 8px;
      padding: 10px 12px;
      background: var(--bg-secondary);
      border-radius: var(--radius-sm);
      margin-bottom: 6px;
      flex-wrap: wrap;
    }

    .status-label {
      font-size: 0.8rem;
      color: var(--text-secondary);
      flex: 1;
      min-width: 0;
    }

    .status-value {
      font-weight: 600;
      font-size: 0.8rem;
      padding: 3px 10px;
      border-radius: 6px;
      white-space: nowrap;
      flex-shrink: 0;
    }

    .status-value.success {
      background: rgba(16, 185, 129, 0.2);
      color: var(--success);
    }

    .status-value.warning {
      background: rgba(245, 158, 11, 0.2);
      color: var(--warning);
    }

    .status-value.info {
      background: rgba(59, 130, 246, 0.2);
      color: var(--info);
    }

    .status-value.danger {
      background: rgba(239, 68, 68, 0.2);
      color: var(--danger);
    }


      ...
  615     .status-row {
  616       display: flex;
  617       justify-content: space-between;
  618       align-items: center;
- 619       padding: 12px;
+ 619       gap: 8px;
+ 620       padding: 10px 12px;
  620       background: var(--bg-secondary);
  621       border-radius: var(--radius-sm);
- 622       margin-bottom: 8px;
+ 623       margin-bottom: 6px;
+ 624       flex-wrap: wrap;
  623     }
  624
  625     .status-label {
- 626       font-size: 0.85rem;
+ 628       font-size: 0.8rem;
  627       color: var(--text-secondary);
+ 630       flex: 1;
+ 631       min-width: 0;
  628     }
  629
  630     .status-value {
  631       font-weight: 600;
- 632       font-size: 0.85rem;
- 633       padding: 4px 12px;
+ 636       font-size: 0.8rem;
+ 637       padding: 3px 10px;
  634       border-radius: 6px;
+ 639       white-space: nowrap;
+ 640       flex-shrink: 0;
  635     }
  636
  637     .status-value.success {
  638       background: rgba(16, 185, 129, 0.2);
      ...


Now fix the floating action bar for mobile (too wide on small screens):

    /* Floating Action Bar */
    .floating-action-bar {
      position: fixed;
      bottom: -100px;
      left: 50%;
      transform: translateX(-50%);
      background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      padding: 16px 24px;
      display: flex;
      align-items: center;
      gap: 20px;
      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
      transition: bottom 0.3s ease;
      z-index: 1000;
    }

    .floating-action-bar.visible {
      bottom: 24px;
    }

    .selection-count {
      font-weight: 600;
      color: var(--text-primary);
      white-space: nowrap;
    }

    .selection-count span {
      color: var(--accent-secondary);
      font-size: 1.2rem;
    }

    .action-buttons {
      display: flex;
      gap: 12px;
    }

    .btn-clear {
      background: transparent;
      color: var(--text-secondary);
      border: 1px solid var(--border);
    }

    .btn-clear:hover {
      background: rgba(255, 255, 255, 0.05);
    }

    /* Floating Action Bar */
    .floating-action-bar {
      position: fixed;
      bottom: -100px;
      left: 12px;
      right: 12px;
      background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      padding: 12px 16px;
      display: flex;
      align-items: center;
      gap: 12px;
      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
      transition: bottom 0.3s ease;
      z-index: 1000;
    }

    .floating-action-bar.visible {
      bottom: 16px;
    }

    .selection-count {
      font-weight: 600;
      color: var(--text-primary);
      white-space: nowrap;
      font-size: 0.85rem;
    }

    .selection-count span {
      color: var(--accent-secondary);
      font-size: 1.1rem;
    }

    .action-buttons {
      display: flex;
      gap: 8px;
      margin-left: auto;
    }

    .btn-clear {
      background: transparent;
      color: var(--text-secondary);
      border: 1px solid var(--border);
    }

    .btn-clear:hover {
      background: rgba(255, 255, 255, 0.05);
    }


      ...
  212     /* Floating Action Bar */
  213     .floating-action-bar {
  214       position: fixed;
  215       bottom: -100px;
- 216       left: 50%;
- 217       transform: translateX(-50%);
+ 216       left: 12px;
+ 217       right: 12px;
  218       background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);
  219       border: 1px solid var(--border);
  220       border-radius: var(--radius);
- 221       padding: 16px 24px;
+ 221       padding: 12px 16px;
  222       display: flex;
  223       align-items: center;
- 224       gap: 20px;
+ 224       gap: 12px;
  225       box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
  226       transition: bottom 0.3s ease;
  227       z-index: 1000;
  228     }
  229
  230     .floating-action-bar.visible {
- 231       bottom: 24px;
+ 231       bottom: 16px;
  232     }
  233
  234     .selection-count {
  235       font-weight: 600;
  236       color: var(--text-primary);
  237       white-space: nowrap;
+ 238       font-size: 0.85rem;
  238     }
  239
  240     .selection-count span {
  241       color: var(--accent-secondary);
- 242       font-size: 1.2rem;
+ 243       font-size: 1.1rem;
  243     }
  244
  245     .action-buttons {
  246       display: flex;
- 247       gap: 12px;
+ 248       gap: 8px;
+ 249       margin-left: auto;
  248     }
  249
  250     .btn-clear {
  251       background: transparent;
      ...


Now update the desktop media queries to polish layout at wider widths:

    /* Desktop Enhancements */
    @media (min-width: 640px) {
      .container {
        padding: 24px;
      }

      header {
        padding: 24px;
      }

      .header-top {
        flex-direction: row;
        justify-content: space-between;
        align-items: center;
      }

      h1 {
        font-size: 1.75rem;
      }

      .summary-cards {
        grid-template-columns: repeat(4, 1fr);
      }

      .municipality-header {
        flex-direction: row;
        justify-content: space-between;
        align-items: center;
      }

      .permits-list {
        padding: 16px;
      }

      .permit-meta {
        gap: 24px;
      }
    }

    @media (min-width: 1024px) {
      .permit-details {
        display: none;
      }

      .permit-card.expanded .permit-details {
        display: grid;
        grid-template-columns: repeat(2, 1fr);
        gap: 1px;
        background: var(--border);
      }

      .permit-card.expanded .permit-details .detail-section {
        background: var(--bg-primary);
        border-bottom: none;
      }
    }

    /* Tablet+ Enhancements */
    @media (min-width: 640px) {
      .container {
        padding: 24px;
      }

      header {
        padding: 24px;
      }

      .header-top {
        flex-direction: row;
        justify-content: space-between;
        align-items: center;
      }

      h1 {
        font-size: 1.75rem;
      }

      .summary-cards {
        grid-template-columns: repeat(4, 1fr);
      }

      .municipality-header {
        flex-direction: row;
        justify-content: space-between;
        align-items: center;
      }

      .permits-list {
        padding: 16px;
      }

      .permit-meta {
        gap: 10px 20px;
      }

      .meta-item {
        font-size: 0.8rem;
      }

      .btn {
        padding: 10px 18px;
        font-size: 0.9rem;
      }

      .floating-action-bar {
        left: 50%;
        right: auto;
        transform: translateX(-50%);
        width: auto;
        min-width: 320px;
        max-width: 560px;
        padding: 14px 24px;
      }

      .floating-action-bar.visible {
        bottom: 24px;
      }
    }

    @media (min-width: 1024px) {
      .permit-details {
        display: none;
      }

      .permit-card.expanded .permit-details {
        display: grid;
        grid-template-columns: repeat(2, 1fr);
        gap: 1px;
        background: var(--border);
      }

      .permit-card.expanded .permit-details .detail-section {
        background: var(--bg-primary);
        border-bottom: none;
      }
    }

    /* Ensure attachment download links don't overflow on mobile */
    .attachment-item {
      flex-wrap: wrap;
    }

    .attachment-info {
      flex: 1;
      min-width: 0;
      max-width: calc(100% - 80px);
    }

    /* Section title sizing on mobile */
    .section-title {
      font-size: 0.8rem;
    }

    /* Professional cards - prevent overflow */
    .professional-name {
      word-break: break-word;
    }

    .professional-license {
      word-break: break-all;
    }

    /* Condition text wrapping */
    .condition-type {
      word-break: break-word;
    }


      ...
  938       gap: 12px;
  939       justify-content: flex-end;
  940     }
  941
- 942     /* Desktop Enhancements */
+ 942     /* Tablet+ Enhancements */
  943     @media (min-width: 640px) {
  944       .container {
  945         padding: 24px;
  946       }
  947
  948       header {
  949         padding: 24px;
  950       }
  951
  952       .header-top {
  953         flex-direction: row;
  954         justify-content: space-between;
  955         align-items: center;
  956       }
  957
  958       h1 {
  959         font-size: 1.75rem;
  960       }
  961
  962       .summary-cards {
  963         grid-template-columns: repeat(4, 1fr);
  964       }
  965
  966       .municipality-header {
  967         flex-direction: row;
  968         justify-content: space-between;
  969         align-items: center;
  970       }
  971
  972       .permits-list {
  973         padding: 16px;
  974       }
  975
  976       .permit-meta {
- 977         gap: 24px;
+ 977         gap: 10px 20px;
  978       }
+ 979
+ 980       .meta-item {
+ 981         font-size: 0.8rem;
+ 982       }
+ 983
+ 984       .btn {
+ 985         padding: 10px 18px;
+ 986         font-size: 0.9rem;
+ 987       }
+ 988
+ 989       .floating-action-bar {
+ 990         left: 50%;
+ 991         right: auto;
+ 992         transform: translateX(-50%);
+ 993         width: auto;
+ 994         min-width: 320px;
+ 995         max-width: 560px;
+ 996         padding: 14px 24px;
+ 997       }
+ 998
+ 999       .floating-action-bar.visible {
+1000         bottom: 24px;
+1001       }
  979     }
  980
  981     @media (min-width: 1024px) {
  982       .permit-details {
  983         display: none;
  984       }
  985
  986       .permit-card.expanded .permit-details {
  987         display: grid;
  988         grid-template-columns: repeat(2, 1fr);
  989         gap: 1px;
  990         background: var(--border);
  991       }
  992
  993       .permit-card.expanded .permit-details .detail-section {
  994         background: var(--bg-primary);
  995         border-bottom: none;
  996       }
  997     }
+1021
+1022     /* Ensure attachment download links don't overflow on mobile */
+1023     .attachment-item {
+1024       flex-wrap: wrap;
+1025     }
+1026
+1027     .attachment-info {
+1028       flex: 1;
+1029       min-width: 0;
+1030       max-width: calc(100% - 80px);
+1031     }
+1032
+1033     /* Section title sizing on mobile */
+1034     .section-title {
+1035       font-size: 0.8rem;
+1036     }
+1037
+1038     /* Professional cards - prevent overflow */
+1039     .professional-name {
+1040       word-break: break-word;
+1041     }
+1042
+1043     .professional-license {
+1044       word-break: break-all;
+1045     }
+1046
+1047     /* Condition text wrapping */
+1048     .condition-type {
+1049       word-break: break-word;
+1050     }
  998   </style>
  999 </head>
 1000 <body>
 1001   <div class="container">
      ...


Conversation info (untrusted metadata):
```json
{
  "message_id": "303",
  "sender": "6558388607"
}
```

also permits button missing from the top on mobile


The user is saying there's a "permits button" missing from the top on mobile. Let me look at the compliance dashboard (pm.ashent.us / ASH Compliance Tracker at /home/crogers2287/wm) since the memory notes mention "Pending: Wire permit tracker as a tab in compliance dashboard (pm.ashent.us)".

Wait, but they said "on mobile" - maybe they mean there's a navigation button or tab that links to the permits page that's missing on mobile. Let me check if this is about the compliance dashboard having a link to the permit tracker, or if it's about the permit tracker page itself missing a button at the top.

Let me re-read the context.

The previous conversation fixed mobile display of the permits page, so now they're pointing out that the compliance dashboard is missing a "Permits" tab or button on mobile—likely the one that's supposed to wire the permit tracker into the dashboard as mentioned in the pending tasks.

Let me check both apps to find the missing button:

exec

find /home/crogers2287/wm -name "*.html" | head -10 && echo "---" && find /home/crogers2287/accela/apps/permit-tracker/public -name "*.html"


/home/crogers2287/wm/templates/project.html
/home/crogers2287/wm/templates/base.html
/home/crogers2287/wm/templates/dashboard.html
/home/crogers2287/wm/templates/weekly_summary.html
/home/crogers2287/wm/templates/permits.html
/home/crogers2287/wm/templates/help.html
---
/home/crogers2287/accela/apps/permit-tracker/public/index.html


grep -n "nav\|permit\|button\|mobile\|hamburger\|menu" /home/crogers2287/wm/templates/base.html | head -60


16:        /* Hide desktop table on mobile, show cards */
19:            .mobile-cards { display: block !important; }
23:            .mobile-cards { display: none !important; }
28:    <nav class="bg-slate-800 text-white p-3 md:p-4 shadow-lg">
34:                <a href="/permits" class="{% if request.url.path == '/permits' %}text-blue-400 font-semibold{% else %}text-gray-300 hover:text-white{% endif %} transition-colors text-sm md:text-base">
45:    </nav>


<!DOCTYPE html>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}ASH Project Status Tracker{% endblock %}</title>

                ASH Project Status

                    📋 Permits

                    Weekly Summary

                    ❓ Help

        {% block content %}{% endblock %}

            ASH Project Status Tracker © 2024


I can see the problem now. In the nav, the "Permits" link uses `hidden sm:inline` for the text span - so on mobile (< sm = < 640px), only the emoji "📋" shows. But the user says the "permits button" is missing entirely on mobile.

Wait, looking more carefully at the nav:

```html
<a href="/permits" class="... transition-colors text-sm md:text-base">
    📋 <span class="hidden sm:inline">Permits</span>
</a>
```

The emoji 📋 should still show, but the word "Permits" is hidden.

The emoji might be too small or hard to see on mobile, or the user could be referring to a different permits interface entirely - maybe they want a link to the external permit tracker app running on port 8007 instead of the built-in permits page on the compliance app. I see the issue now — on mobile, the Permits nav link only shows the 📋 emoji because the text is hidden below the `sm` breakpoint, making it unclear or hard to tap. I should make the Permits text always visible, or at least ensure it's recognizable on smaller screens.

Found it. In the compliance dashboard nav, the "Permits" text is `hidden sm:inline` — so on mobile only the 📋 emoji shows, making it look like an icon with no label. Easy fix:

edit

                    📋 Permits

                    Weekly Summary

                    ❓ Help

                    📋 Permits

                    Weekly Summary

                    ❓ Help


Successfully replaced text in /home/crogers2287/wm/templates/base.html.

...
  29

  30
  31                 ASH Project Status
  32
- 33

+ 33

  34
- 35                     📋 Permits
+ 35                     📋 Permits
  36
  37
  38                     Weekly Summary
  39
     ...


Now let's restart both apps to pick up all changes:

exec

pm2 restart ash-compliance permit-tracker 2>&1 | tail -8


│ 6  │ pasturelog-api           │ default     │ 1.0.0   │ fork    │ N/A      │ 0      │ 167… │ stopped   │ 0%       │ 0b       │ crogers… │ disabled │
│ 7  │ pasturelog-frontend      │ default     │ N/A     │ fork    │ 3562456  │ 12h    │ 0    │ online    │ 0%       │ 60.0mb   │ crogers… │ disabled │
│ 8  │ permit-scheduler         │ default     │ 1.0.0   │ fork    │ 3562457  │ 12h    │ 0    │ online    │ 0%       │ 46.1mb   │ crogers… │ disabled │
│ 9  │ permit-tracker           │ default     │ N/A     │ fork    │ 411313   │ 0s     │ 1    │ online    │ 0%       │ 4.5mb    │ crogers… │ disabled │
│ 12 │ quest-scheduler          │ default     │ N/A     │ fork    │ 3562472  │ 12h    │ 0    │ online    │ 0%       │ 15.8mb   │ crogers… │ disabled │
│ 4  │ t5gemma-vision           │ default     │ N/A     │ fork    │ 3562381  │ 12h    │ 0    │ online    │ 0%       │ 1.1gb    │ crogers… │ disabled │
│ 11 │ tmux-web                 │ default     │ N/A     │ fork    │ 411151   │ 0s     │ 795… │ online    │ 0%       │ 40.5mb   │ crogers… │ disabled │
└────┴──────────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘


pm2 restart ash-compliance 2>&1 | tail -4


│ 12 │ quest-scheduler          │ default     │ N/A     │ fork    │ 3562472  │ 12h    │ 0    │ online    │ 0%       │ 15.8mb   │ crogers… │ disabled │
│ 4  │ t5gemma-vision           │ default     │ N/A     │ fork    │ 3562381  │ 12h    │ 0    │ online    │ 0%       │ 1.1gb    │ crogers… │ disabled │
│ 11 │ tmux-web                 │ default     │ N/A     │ fork    │ 413536   │ 0s     │ 796… │ online    │ 0%       │ 39.9mb   │ crogers… │ disabled │
└────┴──────────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘


pm2 list | grep -E "ash-compliance|permit-tracker"


│ 5  │ ash-compliance           │ default     │ N/A     │ fork    │ 413666   │ 5s     │ 9    │ online    │ 0%       │ 118.5mb  │ crogers… │ disabled │
│ 9  │ permit-tracker           │ default     │ N/A     │ fork    │ 411313   │ 11s    │ 1    │ online    │ 0%       │ 70.3mb   │ crogers… │ disabled │


Both online. Three fixes deployed:

1. **"Permits" nav button** — the label text was `hidden sm:inline`, so on mobile only the 📋 emoji showed (effectively invisible as a tap target). Now always shows "📋 Permits".

2. **License renewal records hidden** — `Add Contractor License To a Record` and `Add Contact to a Building Record` are now excluded from the API query, summary counts, and per-municipality stats at the server level.

3. **Mobile layout improvements** (from previous message) — tighter spacing on permit cards, status badges don't overflow, meta items wrap cleanly, status-row labels handle long text, floating action bar spans full width on small screens.


Conversation info (untrusted metadata):
```json
{
  "message_id": "309",
  "sender": "6558388607"
}
```

can you try and attach the permit info to the cards on the main dash? address or job info is probably similar


The user wants to show permit information on the project cards in the main compliance dashboard (pm.ashent.us). They want to attach permit info (address or job info) to the project cards on the main dashboard.

Let me look at the compliance dashboard code to understand how it's structured - specifically the project cards and how they display data, and then figure out how to match/attach permit data to them.

Let me look at the dashboard and how projects/permits are structured:


{% extends "base.html" %}
{% block title %}Dashboard - ASH Project Status Tracker{% endblock %}

{% block content %}

Project Status

            {{ total_projects }} projects | Last scan: {{ last_scan }}

            🔄 Rescan

            Logout

        {{ active_projects|length }} Active

        {{ marketing_projects|length }} Bidding

{% if scan_in_progress and total_projects == 0 %}

    <svg class="animate-spin h-10 w-10 text-blue-500 mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
    </svg>

Scanning Project Folders

This may take a minute over the network...

{% else %}

        Active Projects
        (sorted by activity)

        {% for project in active_projects|sort(attribute='total_proposals', reverse=true) %}

                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
                    </svg>

                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"/>
                    </svg>

                                {{ project.name }}

                                {% if project.checklist and project.checklist.municipality %}
                                    {{ project.checklist.municipality }}
                                {% endif %}
                                {% if project.checklist and project.checklist.permit_number %}
                                    {% if project.checklist.municipality %}|{% endif %}
                                    #{{ project.checklist.permit_number }}
                                {% endif %}

                    {% set plumbing_sub = project.get_mep_sub('Plumbing') %}
                    {% set hvac_sub = project.get_mep_sub('HVAC') %}
                    {% set elec_sub = project.get_mep_sub('Electrical') %}

                                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"/>
                                </svg>

Plumb

                                {{ plumbing_sub or '—' }}

                                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
                                </svg>

HVAC

                                {{ hvac_sub or '—' }}

                                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
                                </svg>

Elec

                                {{ elec_sub or '—' }}

                    {{ project.trades_compliant }}/{{ project.trades|length }} trades · {{ project.total_proposals }} bids

                        = 90 %}bg-emerald-500{% elif project.score >= 70 %}bg-amber-500{% else %}bg-gray-300{% endif %}">
                        = 90 %}text-emerald-600{% elif project.score >= 70 %}text-amber-600{% else %}text-gray-400{% endif %}">{{ "%.0f"|format(project.score) }}%

        {% endfor %}

        Bidding Phase
        (sorted by activity)

        {% for project in marketing_projects|sort(attribute='total_proposals', reverse=true) %}

                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
                    </svg>

                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"/>
                    </svg>

                                {{ project.name }}

                                {% if project.checklist and project.checklist.municipality %}
                                    {{ project.checklist.municipality }}
                                {% endif %}
                                {% if project.checklist and project.checklist.permit_number %}
                                    {% if project.checklist.municipality %}|{% endif %}
                                    #{{ project.checklist.permit_number }}
                                {% endif %}

                    {% set plumbing_sub = project.get_mep_sub('Plumbing') %}
                    {% set hvac_sub = project.get_mep_sub('HVAC') %}
                    {% set elec_sub = project.get_mep_sub('Electrical') %}

                                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"/>
                                </svg>

Plumb

                                {{ plumbing_sub or '—' }}

                                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
                                </svg>

HVAC

                                {{ hvac_sub or '—' }}

                                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
                                </svg>

Elec

                                {{ elec_sub or '—' }}

                    {{ project.trades_compliant }}/{{ project.trades|length }} trades · {{ project.total_proposals }} bids

                        = 90 %}bg-emerald-500{% elif project.score >= 70 %}bg-amber-500{% else %}bg-gray-300{% endif %}">
                        = 90 %}text-emerald-600{% elif project.score >= 70 %}text-amber-600{% else %}text-gray-400{% endif %}">{{ "%.0f"|format(project.score) }}%

        {% endfor %}

{% endif %}

{% endblock %}


I can see that:
1. Each project card already has `project.checklist.permit_number` - a permit number stored in the checklist
2. The permit tracker has a database of permits with addresses, statuses, etc.
3. I need to match projects to permits and show permit info on the dashboard cards

The approach:
1. In the backend (`watcher.py` or `app.py` or wherever the dashboard route is), I need to query the permit tracker's database and match permits to projects by permit number or address
2. Then pass that matched permit data to the template
3. In the template, render the permit info (status, address) on the card

4. Now I'm examining the backend to see how the dashboard route is structured and where I can hook in the permit lookup logic.

Now let me see how the dashboard route works and where permit data could be injected:

find /home/crogers2287/wm -name "*.py" | grep -v __pycache__ | xargs ls -la


-rw-rw-r-- 1 crogers2287 crogers2287 10057 Feb 18 18:52 /home/crogers2287/wm/accela_client.py
-rw-r--r-- 1 crogers2287 crogers2287  3872 Jan 27 14:43 /home/crogers2287/wm/ai_engine.py
-rw-r--r-- 1 crogers2287 crogers2287 16114 Jan 14 18:55 /home/crogers2287/wm/checklist_parser.py
-rw-r--r-- 1 crogers2287 crogers2287 15053 Feb 18 18:45 /home/crogers2287/wm/database.py
-rw-r--r-- 1 crogers2287 crogers2287 48596 Feb 18 18:46 /home/crogers2287/wm/main.py
-rw-r--r-- 1 crogers2287 crogers2287  9247 Jan 26 19:44 /home/crogers2287/wm/models.py
-rw-r--r-- 1 crogers2287 crogers2287 17881 Feb 18 17:22 /home/crogers2287/wm/scanner.py
-rw------- 1 crogers2287 crogers2287  7389 Feb 18 17:01 /home/crogers2287/wm/watcher.py


grep -n "dashboard\|def get_\|permit_number\|checklist\|permit" /home/crogers2287/wm/main.py | head -50


27:def get_eastern_now():
55:def get_share_data(share_id: str) -> dict | None:
278:async def dashboard(request: Request):
279:    """Main dashboard showing all projects"""
306:    return templates.TemplateResponse("dashboard.html", {
338:    checklist = project.get_compliance_checklist()
345:        "checklist": checklist
350:async def get_gap_analysis(project_name: str):
478:            document.getElementById('sub').textContent = 'WebSocket unavailable — refresh the dashboard manually when done.';
495:    # Generate checklist (no AI gap analysis)
497:    checklist = project.get_compliance_checklist()
503:        html_content = render_pdf_template(project, gap_analysis, checklist, project.checklist)
517:        html_content = render_pdf_template(project, gap_analysis, checklist, project.checklist)
590:    checklist = project.get_compliance_checklist()
593:    html_content = render_pdf_template(project, gap_analysis, checklist, project.checklist)
646:def render_pdf_template(project: ProjectCompliance, gap_analysis: str, checklist: list, job_checklist=None) -> str:
662:    # Build checklist HTML
663:    checklist_html = ""
664:    if checklist:
665:        for item in checklist[:6]:  # Limit to 6 items for space
669:            checklist_html += f'''
677:        checklist_html = '
✓ All requirements met!
'
718:    if job_checklist and hasattr(job_checklist, 'tasks') and job_checklist.tasks:
720:        for task in job_checklist.tasks:
728:        if hasattr(job_checklist, 'subcontractors') and job_checklist.subcontractors:
729:            for sub in job_checklist.subcontractors:
734:

735:
📋 Job Setup Checklist ({job_checklist.tasks_completed}/{job_checklist.tasks_total} Complete)

736:

738:
PM: {job_checklist.project_manager or "N/A"}

739:
Supt: {job_checklist.superintendent or "N/A"}

740:
Permit #: {job_checklist.permit_number or "N/A"}

741:
Estimator: {job_checklist.estimator or "N/A"}

746:

752:
Subcontractors ({len(job_checklist.subcontractors) if hasattr(job_checklist, "subcontractors") else 0})

753:

776:    permit_status = "ISSUED" if project.permit_found else "MISSING"
777:    permit_class = "pill-green" if project.permit_found else "pill-red"
813:        .checklist-box {{ flex: 1; background: linear-gradient(135deg, #f093fb11 0%, #f5576c11 100%); border: 1px solid #f5576c44; border-radius: 6px; padding: 10px; }}
836:        .job-checklist-section {{
843:        .job-checklist-header {{
850:        .job-checklist-content {{
878:        .checklist-table.compact {{
881:        .checklist-table.compact th, .checklist-table.compact td {{
895:        .checklist-table {{
900:        .checklist-table th {{
907:        .checklist-table td {{
944:                {permit_status}
975:

980:            {checklist_html}


async def dashboard(request: Request):
    """Main dashboard showing all projects"""
    # Check authentication
    if not check_auth(request):
        return RedirectResponse(url="/login", status_code=303)
    # ALWAYS load from cache first - never block on SMB scan
    projects = await database.get_all_projects()

    # If cache is empty, trigger background scan (don't wait for it)
    needs_scan = not projects
    if needs_scan:
        asyncio.create_task(background_scan())

    # Separate by status
    marketing_projects = [p for p in projects if p.status == ProjectStatus.MARKETING]
    active_projects = [p for p in projects if p.status == ProjectStatus.ACTIVE]

    # Stats
    total_compliant = len([p for p in projects if p.score >= 90])
    total_at_risk = len([p for p in projects if p.score < 70])

    # Get most active projects for quick access sections
    loop = asyncio.get_event_loop()
    most_active_projects, most_active_marketing = await asyncio.gather(
        loop.run_in_executor(None, scanner.get_most_active_by_status, ProjectStatus.ACTIVE, 5),
        loop.run_in_executor(None, scanner.get_most_active_by_status, ProjectStatus.MARKETING, 5)
    )

    return templates.TemplateResponse("dashboard.html", {
        "request": request,
        "marketing_projects": marketing_projects,
        "active_projects": active_projects,
        "total_projects": len(projects),
        "total_compliant": total_compliant,
        "total_at_risk": total_at_risk,
        "last_scan": get_eastern_now().strftime("%Y-%m-%d %I:%M %p ET"),
        "scan_in_progress": needs_scan or _scan_in_progress,
        "most_active_projects": most_active_projects,
        "most_active_marketing": most_active_marketing
    })

[874 more lines in file. Use offset=318 to continue.]


Now let me understand how the matching would work. The project cards have:
1. `project.name` - the project folder name
2. `project.checklist.permit_number` - a permit number if it's in the checklist
3. `project.checklist.municipality` - the municipality

The permit tracker DB has:
- `record_number` - the permit number (e.g., "TB25-12345")
- `address` - the property address
- `status` - permit status
- `record_type` - type of permit

Strategy for matching:
1. **By permit number**: If `project.checklist.permit_number` matches a `record_number` in the permit DB - this is the most reliable

2. **By address/name**: If the project name contains part of the permit address or vice versa - less reliable but useful as a fallback

I should examine what format the permit numbers actually take in both systems to see if there are any transformations needed for matching.

Let me check how permit numbers look in both systems and sample some project names/addresses:

cd /home/crogers2287/accela/apps/permit-tracker && python3 -c "
import sqlite3
conn = sqlite3.connect('data/permits.db')
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute('SELECT record_number, address, status, record_type FROM permits WHERE address != \"\" LIMIT 15')
for r in cur.fetchall():
    print(r['record_number'], '|', r['address'][:50], '|', r['status'])
conn.close()
"


BLD-26-0520379 | 1509 E Fowler Ave T 33612 | In Process
BLD-25-0518834 | 7401 E Broadway Ave T 33619 | Issued
BLD-25-0518760 | 7401 E Broadway Ave T 33619 | Withdrawn
BDE-25-0444898 | 1509 E Fowler Ave T 33612 | Issued
BLD-25-0516396 | 1622 W Kennedy Blvd T 33606 | Issued
UTL-25-0000708 | 4705 W Knights Ave T 33611 | In Process
BLD2509-1098 | 8730 US 301 N 113 PARRISH 34219 | Permit Issued
HC-BLD-23-0048794 | 4911 N US Highway 301, Tampa FL 33610 | Expired
HC-BLD-23-0047958 | 5901 Memorial Hwy, Tampa FL 33615 | Expired
HC-BLD-21-0017028 | 6401 N 54th St, Tampa FL 33610 | Expired
HC-BLD-26-0080044 | 1995 E State Road 60, Valrico FL 33594 | Issued
HC-BLD-25-0079296 | 13135 S US Highway 301, Riverview FL 33578 | Issued
HC-BLD-25-0076489 | 402 S US Highway 41, Ruskin FL 33570 | Awaiting Client Reply
HC-BLD-25-0072761 | 2001 Vandervort Rd, Lutz FL 33549 | In Process
HC-BLD-21-0007452 | 14806 N 12th St, Lutz 33549 | Awaiting Client Reply


cd /home/crogers2287/wm && python3 -c "
import asyncio, sys
sys.path.insert(0, '.')
import database

async def run():
    projects = await database.get_all_projects()
    for p in projects[:15]:
        cl = p.checklist
        pnum = getattr(cl, 'permit_number', None) if cl else None
        muni = getattr(cl, 'municipality', None) if cl else None
        print(repr(p.name[:40]), '| permit:', pnum, '| muni:', muni)

asyncio.run(run())
"


Traceback (most recent call last):
  File "<string>", line 4, in <module>
  File "/home/crogers2287/wm/database.py", line 2, in <module>
    import aiosqlite
ModuleNotFoundError: No module named 'aiosqlite'

(Command exited with code 1)


cd /home/crogers2287/wm && python3 -c "
import sqlite3, json
conn = sqlite3.connect('data/projects.db')
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute('SELECT name FROM sqlite_master WHERE type=\"table\"')
print([r['name'] for r in cur.fetchall()])
" 2>/dev/null || ls /home/crogers2287/wm/data/ 2>/dev/null || echo "no data dir"


no data dir


grep -n "sqlite\|database\|db_path\|DB_PATH\|\.db" /home/crogers2287/wm/database.py | head -20


2:import aiosqlite
9:DB_PATH = Path(__file__).parent / "compliance.db"
13:    """Initialize the database with WAL mode for better concurrency"""
14:    async with aiosqlite.connect(DB_PATH, timeout=3.0) as db:
94:    """Save or update a project in the database"""
95:    async with aiosqlite.connect(DB_PATH, timeout=3.0) as db:
120:    async with aiosqlite.connect(DB_PATH, timeout=3.0) as db:
121:        db.row_factory = aiosqlite.Row
154:    import sqlite3
158:        conn = sqlite3.connect(DB_PATH)
159:        conn.row_factory = sqlite3.Row
202:    async with aiosqlite.connect(DB_PATH, timeout=3.0) as db:
203:        db.row_factory = aiosqlite.Row
233:    async with aiosqlite.connect(DB_PATH, timeout=3.0) as db:
250:    async with aiosqlite.connect(DB_PATH, timeout=3.0) as db:
266:    async with aiosqlite.connect(DB_PATH, timeout=3.0) as db:
273:    async with aiosqlite.connect(DB_PATH, timeout=10.0) as db:
316:    async with aiosqlite.connect(DB_PATH, timeout=3.0) as db:
317:        db.row_factory = aiosqlite.Row
336:    async with aiosqlite.connect(DB_PATH, timeout=3.0) as db:


cd /home/crogers2287/wm && python3 -c "
import sqlite3, json
conn = sqlite3.connect('compliance.db')
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute('SELECT name FROM sqlite_master WHERE type=\"table\"')
print([r[0] for r in cur.fetchall()])
cur.execute('PRAGMA table_info(projects)')
print([r[1] for r in cur.fetchall()])
"


['projects', 'scan_history', 'sqlite_sequence', 'gap_analysis', 'permits', 'permit_sync_log']
['name', 'path', 'status', 'noc_found', 'permit_found', 'noc_file', 'permit_file', 'trades_json', 'trades_with_folders', 'trades_compliant', 'total_proposals', 'sublist_found', 'sublist_file', 'checklist_json', 'checklist_file', 'score', 'compliance_status', 'last_scanned', 'last_modified']


cd /home/crogers2287/wm && python3 -c "
import sqlite3, json
conn = sqlite3.connect('compliance.db')
conn.row_factory = sqlite3.Row
cur = conn.cursor()
# Check the permits table in compliance db
cur.execute('PRAGMA table_info(permits)')
print('permits cols:', [r[1] for r in cur.fetchall()])
cur.execute('SELECT * FROM permits LIMIT 3')
for r in cur.fetchall():
    print(dict(r))
"


permits cols: ['record_id', 'custom_id', 'record_type', 'module', 'address', 'status', 'status_class', 'opened_date', 'updated_date', 'description', 'job_value', 'documents_json', 'doc_count', 'has_noc', 'href', 'last_synced', 'agency', 'municipality_label', 'has_ntbo', 'has_private_provider']
{'record_id': 'HCFL-26CAP-00000-00BW4', 'custom_id': 'HC-BTR-26-0309234', 'record_type': 'Commercial Electrical Trade Permit', 'module': 'Building', 'address': 'Store was offline for a little while, and someone stole the electrical panel guts and feeder wire. This permit is to restore functionality to the 1200 amp service to get the store electrical operational.', 'status': 'Issued', 'status_class': 'blue', 'opened_date': '2026-02-16 00:00:00', 'updated_date': '2026-02-16 07:56:03', 'description': 'Store was offline for a little while, and someone stole the electrical panel guts and feeder wire. This permit is to restore functionality to the 1200 amp service to get the store electrical operational.', 'job_value': 0.0, 'documents_json': '[{"id": 6234976, "fileName": "PermitPlacard_EMSE_20260216_075608.pdf", "category": "Permit", "description": "", "status": "Uploaded", "uploadedDate": "2026-02-16 07:56:08", "size": 114470.0, "downloadUrl": "/permits/document/6234976?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": false}, {"id": 6234977, "fileName": "ELECT(1).pdf", "category": "Supporting Documents", "description": "Electrical plans", "status": "Uploaded", "uploadedDate": "2026-02-16 07:55:03", "size": 882534.0, "downloadUrl": "/permits/document/6234977?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": false}]', 'doc_count': 2, 'has_noc': 0, 'href': 'https://aca-prod.accela.com/HCFL/Cap/CapDetail.aspx?Module=Building&TabName=Building&agencyCode=HCFL', 'last_synced': '2026-02-18T18:52:41.440358', 'agency': 'HCFL', 'municipality_label': 'Hillsborough County', 'has_ntbo': 0, 'has_private_provider': 0}
{'record_id': 'HCFL-26CAP-00000-0021H', 'custom_id': 'HC-BLD-26-0080044', 'record_type': 'Commercial Building Alterations (Renovations)', 'module': 'Building', 'address': 'Private Provider Plan Review and Inspections- Interior renovation of existing tenant space (2,100 SF) Healthsource Chiropractic office buildout', 'status': 'Issued', 'status_class': 'blue', 'opened_date': '2026-01-09 00:00:00', 'updated_date': '2026-01-09 11:55:13', 'description': 'Private Provider Plan Review and Inspections- Interior renovation of existing tenant space (2,100 SF) Healthsource Chiropractic office buildout', 'job_value': 0.0, 'documents_json': '[{"id": 6115274, "fileName": "Notice to Building Official.pdf", "category": "Private Provider - Notice to Building Official", "description": "", "status": "Uploaded", "uploadedDate": "2026-01-09 11:57:12", "size": 273562.0, "downloadUrl": "/permits/document/6115274?agency=HCFL", "is_noc": false, "is_ntbo": true, "is_private_provider": true}, {"id": 6115283, "fileName": "Private Provider Package.pdf", "category": "Private Provider Documentation", "description": "", "status": "Uploaded", "uploadedDate": "2026-01-09 11:58:20", "size": 26456965.0, "downloadUrl": "/permits/document/6115283?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": true}, {"id": 6124600, "fileName": "Notice to Building Official.pdf", "category": "Private Provider - Notice to Building Official", "description": "We are signing as owners contractor, info on second page is GC info, per form requirements.", "status": "Uploaded", "uploadedDate": "2026-01-13 11:02:17", "size": 280136.0, "downloadUrl": "/permits/document/6124600?agency=HCFL", "is_noc": false, "is_ntbo": true, "is_private_provider": true}, {"id": 6115273, "fileName": "25-117 Health Source of Valrico AE PERMIT REV ss.pdf", "category": "Preliminary Secured Drawings", "description": "", "status": "Uploaded", "uploadedDate": "2026-01-09 11:57:05", "size": 1453435.0, "downloadUrl": "/permits/document/6115273?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": false}, {"id": 6114518, "fileName": "Health Source Valrico E-CALC SS.pdf", "category": "Preliminary Secured Drawings", "description": "", "status": "Uploaded", "uploadedDate": "2026-01-09 11:57:05", "size": 718160.0, "downloadUrl": "/permits/document/6114518?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": false}, {"id": 6115275, "fileName": "HEALTHSOURCE - ELEC - PERMIT - SS 112425.pdf", "category": "Preliminary Secured Drawings", "description": "", "status": "Uploaded", "uploadedDate": "2026-01-09 11:57:17", "size": 4641018.0, "downloadUrl": "/permits/document/6115275?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": false}, {"id": 6115293, "fileName": "HEALTHSOURCE - MP - PERMIT SS.pdf", "category": "Preliminary Secured Drawings", "description": "", "status": "Uploaded", "uploadedDate": "2026-01-09 11:57:13", "size": 1495356.0, "downloadUrl": "/permits/document/6115293?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": false}, {"id": 6126752, "fileName": "APPROVED-HC-BLD-26-0080044.pdf", "category": "Approved Plans - Secured", "description": "Approved plans for permit HC-BLD-26-0080044", "status": "Uploaded", "uploadedDate": "2026-01-13 15:45:22", "size": 5741480.0, "downloadUrl": "/permits/document/6126752?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": false}, {"id": 6125729, "fileName": "Permit_Status_Report_20260113_154523.pdf", "category": "Permit Status Report", "description": "", "status": "Uploaded", "uploadedDate": "2026-01-13 15:45:24", "size": 125179.0, "downloadUrl": "/permits/document/6125729?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": false}, {"id": 6132298, "fileName": "PermitPlacard_20260115_083606.pdf", "category": "Permit", "description": "", "status": "Uploaded", "uploadedDate": "2026-01-15 08:36:07", "size": 114366.0, "downloadUrl": "/permits/document/6132298?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": false}, {"id": 6133038, "fileName": "PP NTBO", "category": "Private Provider - Notice to Building Official", "description": "folio # correction", "status": "Uploaded", "uploadedDate": "2026-01-15 09:28:06", "size": 284365.0, "downloadUrl": "/permits/document/6133038?agency=HCFL", "is_noc": false, "is_ntbo": true, "is_private_provider": true}, {"id": 6141878, "fileName": "RECORDED NOC HEALTHSOURCE.pdf", "category": "Notice of Commencement", "description": "", "status": "Uploaded", "uploadedDate": "2026-01-19 10:39:41", "size": 140596.0, "downloadUrl": "/permits/document/6141878?agency=HCFL", "is_noc": true, "is_ntbo": false, "is_private_provider": false}, {"id": 6186905, "fileName": "5125461 Electrical Rough 20260130 Partial.pdf", "category": "Private Provider - Periodic Inspection Report", "description": "Electrical Rough 20260130 Partial", "status": "Uploaded", "uploadedDate": "2026-02-02 09:52:37", "size": 88550.0, "downloadUrl": "/permits/document/6186905?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": true}, {"id": 6206021, "fileName": "5125461 Electrical UG Slab 20260205 PASS.pdf", "category": "Private Provider - Periodic Inspection Report", "description": "Electrical UG Slab 20260205 PASS", "status": "Uploaded", "uploadedDate": "2026-02-06 10:14:20", "size": 85766.0, "downloadUrl": "/permits/document/6206021?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": true}, {"id": 6206202, "fileName": "5125461 Plumbing UG 20260204 PASS.pdf", "category": "Private Provider - Periodic Inspection Report", "description": "Plumbing UG 20260204 PASS", "status": "Uploaded", "uploadedDate": "2026-02-06 10:14:23", "size": 86317.0, "downloadUrl": "/permits/document/6206202?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": true}, {"id": 6206203, "fileName": "5125461 Slab 20260205 PASS.pdf", "category": "Private Provider - Periodic Inspection Report", "description": "Slab 20260205 PASS", "status": "Uploaded", "uploadedDate": "2026-02-06 10:14:25", "size": 86417.0, "downloadUrl": "/permits/document/6206203?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": true}, {"id": 6236162, "fileName": "5125461 Plumbing Rough 20260213 Partial.pdf", "category": "Private Provider - Periodic Inspection Report", "description": "Plumbing Rough 20260213 Partial", "status": "Uploaded", "uploadedDate": "2026-02-16 10:28:59", "size": 86854.0, "downloadUrl": "/permits/document/6236162?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": true}, {"id": 6245754, "fileName": "5125461 Electrical Rough 20260217 PASS.pdf", "category": "Private Provider - Periodic Inspection Report", "description": "Electrical Rough 20260217 PASS", "status": "Uploaded", "uploadedDate": "2026-02-18 10:49:47", "size": 85749.0, "downloadUrl": "/permits/document/6245754?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": true}]', 'doc_count': 18, 'has_noc': 1, 'href': 'https://aca-prod.accela.com/HCFL/Cap/CapDetail.aspx?Module=Building&TabName=Building&agencyCode=HCFL', 'last_synced': '2026-02-18T18:52:41.440948', 'agency': 'HCFL', 'municipality_label': 'Hillsborough County', 'has_ntbo': 1, 'has_private_provider': 1}
{'record_id': 'HCFL-25CAP-00000-02UXC', 'custom_id': 'HC-BDE-25-0007522', 'record_type': 'Commercial Demolition Permit', 'module': 'Building', 'address': 'Interior Demo of existing convenience store', 'status': 'Cancel', 'status_class': 'yellow', 'opened_date': '2025-12-09 00:00:00', 'updated_date': '2025-12-09 17:01:36', 'description': 'Interior Demo of existing convenience store', 'job_value': 0.0, 'documents_json': '[{"id": 6037137, "fileName": "ASH Letterhead - Multi Page.docx", "category": "Cancellation Letter", "description": "Was looking for early start permit, this permit was applied for in error", "status": "Uploaded", "uploadedDate": "2025-12-10 11:37:21", "size": 59147.0, "downloadUrl": "/permits/document/6037137?agency=HCFL", "is_noc": false, "is_ntbo": false, "is_private_provider": false}]', 'doc_count': 1, 'has_noc': 0, 'href': 'https://aca-prod.accela.com/HCFL/Cap/CapDetail.aspx?Module=Building&TabName=Building&agencyCode=HCFL', 'last_synced': '2026-02-18T18:52:41.441158', 'agency': 'HCFL', 'municipality_label': 'Hillsborough County', 'has_ntbo': 0, 'has_private_provider': 0}


So the compliance DB already has a `permits` table with `custom_id` (like `HC-BLD-26-0080044`), `address`, `status`, `description`, etc.

Now I need to understand how to link projects to permits. Let me look at the checklist structure to see what permit_number looks like.

There's already a `permits` table in the compliance DB. Now let me see how projects link to permits and what the checklist permit numbers look like:

cd /home/crogers2287/wm && python3 -c "
import sqlite3, json
conn = sqlite3.connect('compliance.db')
conn.row_factory = sqlite3.Row
cur = conn.cursor()
# Look at projects with checklist data
cur.execute('SELECT name, checklist_json FROM projects WHERE checklist_json IS NOT NULL LIMIT 10')
for r in cur.fetchall():
    cl = json.loads(r['checklist_json']) if r['checklist_json'] else {}
    pnum = cl.get('permit_number', '')
    muni = cl.get('municipality', '')
    if pnum or muni:
        print(repr(r['name'][:40]), '| permit:', pnum, '| muni:', muni)
conn.close()
"


(no output)


cd /home/crogers2287/wm && python3 -c "
import sqlite3, json
conn = sqlite3.connect('compliance.db')
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute('SELECT name, checklist_json FROM projects LIMIT 5')
for r in cur.fetchall():
    cl = json.loads(r['checklist_json']) if r['checklist_json'] else {}
    print(repr(r['name'][:50]))
    print('  keys:', list(cl.keys())[:10])
conn.close()
"


'21-020 Purple Square Mgmt'
  keys: []
'24-019 APEC Kathleen Shell Lakeland'
  keys: []
'24-024 APEC Hurricane Milton - St. Cloud Citgo Dur'
  keys: ['job_name', 'project_manager', 'superintendent', 'permit_number', 'municipality', 'electric_utility', 'water_utility', 'estimator', 'tasks', 'subcontractors']
'24-027 APEC Galloway Shell Lakeland Old Kathleen'
  keys: ['job_name', 'project_manager', 'superintendent', 'permit_number', 'municipality', 'electric_utility', 'water_utility', 'estimator', 'tasks', 'subcontractors']
'24-028 APEC Mayberry Mariner Mobil'
  keys: ['job_name', 'project_manager', 'superintendent', 'permit_number', 'municipality', 'electric_utility', 'water_utility', 'estimator', 'tasks', 'subcontractors']


cd /home/crogers2287/wm && python3 -c "
import sqlite3, json
conn = sqlite3.connect('compliance.db')
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute('SELECT name, checklist_json FROM projects WHERE checklist_json IS NOT NULL ORDER BY last_scanned DESC')
for r in cur.fetchall():
    cl = json.loads(r['checklist_json']) if r['checklist_json'] else {}
    pnum = cl.get('permit_number', '')
    muni = cl.get('municipality', '')
    if pnum:
        print(repr(r['name'][:45]), '| permit:', repr(pnum), '| muni:', muni)
conn.close()
"


'26-003 Health Source Valrico Fl' | permit: 'TBD' | muni: Hillsborough
'25-040 Swig Dynamic Blvd Wesley Chapel' | permit: 'COMNEW-2025-000416' | muni: Pasco
'25-038 DD Mulberry Sheppard RD' | permit: 'BC-2025-1333' | muni: Polk County
'25-037 Chevron - Ruskin Remodel' | permit: 'HC-BLD-25-0076489' | muni: Hillsborough County
'25-032 DD Broad Street Brooksville' | permit: '25-0004' | muni: City of Brooksville
'25-033 BWW 2 Go Lakeland Harden Blvd' | permit: 'BLD25-06543' | muni: City of Lakeland
'25-031 CSC Parish Fl' | permit: 'BLD2509-1098' | muni: Manatee County
'25-029 Swig Drinks wingshooter' | permit: 'COMNEW-2025-000341' | muni: Pasco
'25-019 Caribou Coffee Clearwater' | permit: 'BCP2025-070725' | muni: Clearwater


cd /home/crogers2287/wm && python3 -c "
import sqlite3, json
conn = sqlite3.connect('compliance.db')
conn.row_factory = sqlite3.Row
cur = conn.cursor()
# Check what permit custom_ids look like vs checklist permit numbers
cur.execute('SELECT custom_id, address, status, description FROM permits LIMIT 10')
for r in cur.fetchall():
    print(r['custom_id'], '|', r['address'][:50], '|', r['status'])
conn.close()
"


HC-BTR-26-0309234 | Store was offline for a little while, and someone  | Issued
HC-BLD-26-0080044 | Private Provider Plan Review and Inspections- Inte | Issued
HC-BDE-25-0007522 | Interior Demo of existing convenience store | Cancel
HC-BLD-25-0076489 | Private Provider Inspection / PI# 7470 Level 3 Alt | Awaiting Client Reply
HC-BLD-25-0072761 | Pole Barn Accessory Structure, With Electric and P | In Process
HC-BLD-25-0070937 | Hurrican Milton storm damage repair, new roof, blo | In Process
CBC1265326 |  | Active
HC-BTR-24-0231567 | Reroof of Residence Re-issued - Original Issued Da | About to Expire
HC-BLD-22-0028802-4 | Requesting a TCO per the fire marshall. | Awaiting Client Reply
CCC1330949 |  | Active


Now I understand the full picture:

1. The compliance DB has both `projects` and `permits` tables
2. Project checklists have `permit_number` fields like `HC-BLD-25-0076489`, `BLD2509-1098`, etc.
3. The permits table has `custom_id` which matches the checklist permit numbers (e.g., `HC-BLD-25-0076489`)
4. The permits table also has the description in the `address` field (which seems wrong - actually looking at the first example the address field seems to contain the description, or possibly this is because the Accela API uses description for address sometimes)

5. The `address` field actually contains job descriptions like "Private Provider Plan Review and Inspections- Interior renovation of existing tenant space (2,100 SF) Healthsource Chiropractic office buildout" rather than actual addresses, which might indicate a scraper issue

So my matching strategy is straightforward: join `projects.checklist_json.permit_number` with `permits.custom_id` to pull in the permit status, description, and other relevant details.

Now I'm checking whether the separate permit tracker database at `/home/crogers2287/accela/apps/permit-tracker/data/permits.db` has cleaner address data. It stores records with fields like `record_number` (e.g., `BLD-26-0520379`), proper `address` values (e.g., `1509 E Fowler Ave T 33612`), and `status` (e.g., `In Process`). The compliance DB uses `custom_id` instead and has different status values like `Awaiting Client Reply`. Since both databases sync with Accela but cover different jurisdictions—the permit tracker handles Tampa, Hillsborough, Manatee, and Pasco while the compliance DB syncs separately through `accela_client.py`—I need to figure out how to integrate them for the dashboard.

The best approach is to use the compliance DB's permits table and match records by `custom_id` against the checklist's `permit_number`, then display the permit status and description on the card. But I'm noticing the `address` field in the compliance DB seems to contain the permit description rather than actual address data, so I need to verify the schema to understand what fields are actually available.

cd /home/crogers2287/wm && python3 -c "
import sqlite3, json
conn = sqlite3.connect('compliance.db')
conn.row_factory = sqlite3.Row
cur = conn.cursor()
# Find matches between checklist permit numbers and permits table
cur.execute('''
    SELECT p.name, p.checklist_json, pm.custom_id, pm.status, pm.address, pm.description
    FROM projects p
    JOIN permits pm ON json_extract(p.checklist_json, \"$.permit_number\") = pm.custom_id
    LIMIT 10
''')
for r in cur.fetchall():
    cl = json.loads(r['checklist_json'])
    print(r['name'][:40], '| match:', r['custom_id'], '| status:', r['status'])
    print('   addr:', r['address'][:60])
conn.close()
"


25-029 Swig Drinks wingshooter | match: COMNEW-2025-000341 | status: Closed
   addr: (PP)  New Construction of Swig Drinks Including Dumpster Enc
25-031 CSC Parish Fl | match: BLD2509-1098 | status: Permit Issued
   addr: NCRQD: **PP/PR NOVA ENG**  Interior buildout of a new Chicke
25-037 Chevron - Ruskin Remodel | match: HC-BLD-25-0076489 | status: Awaiting Client Reply
   addr: Private Provider Inspection / PI# 7470 Level 3 Alterations,
25-040 Swig Dynamic Blvd Wesley Chapel | match: COMNEW-2025-000416 | status: Permit Issued
   addr: (PP)  Construction of a new single story masonry structure


The matching works. The `address` field in the permits table actually contains the job description (not the physical address). That's what's available.

Now for the dashboard, I want to show:
1. Permit status (e.g., "Issued", "Awaiting Client Reply", "In Process")
2. Description/address snippet

The best approach is:
1. In `main.py`'s `dashboard` function, query the compliance DB for permit matches and attach them to each project
2. Show them in the card template

Let me look at the models to understand how to attach this data to projects, and the best way to pass it to the template.

Actually, I think the cleanest approach is to fetch all permit data in a single query and pass it to the template as a lookup dictionary keyed by permit number, then have the template reference it directly. Let me check the project model structure first.

Matches work. Now let me look at the models and how to pass permit data through:

grep -n "class ProjectCompliance\|permit_number\|checklist\b" /home/crogers2287/wm/models.py | head -30


18:    """Task from the job setup checklist"""
27:    """Subcontractor from the checklist"""
36:    """Parsed job setup checklist data"""
40:    permit_number: str = ""
62:class ProjectCompliance(BaseModel):
84:    checklist: Optional[ChecklistData] = None
192:    def get_compliance_checklist(self) -> list[dict]:
193:        """Generate checklist of items needed for 100% compliance"""
194:        checklist = []
198:            checklist.append({
205:            checklist.append({
214:            checklist.append({
227:                    checklist.append({
235:                checklist.append({
242:        return checklist
247:        if not self.checklist or not hasattr(self.checklist, "subcontractors"):
261:        for sub in self.checklist.subcontractors:


Good. The cleanest approach: query the permits table in `main.py`'s dashboard function, build a `{permit_number: permit_row}` dict, pass it to the template. No model changes needed.


"""ASH Compliance Tracker - Main Application"""
import os
import re
import hashlib
import secrets
from pathlib import Path
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from contextlib import asynccontextmanager

import asyncio
from fastapi import FastAPI, Request, HTTPException, WebSocket, WebSocketDisconnect, Form
from fastapi.responses import HTMLResponse, Response, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

import scanner
import database
import ai_engine
import accela_client
from watcher import watcher
from models import ProjectCompliance, ProjectStatus

# Eastern Time zone
EASTERN = ZoneInfo("America/New_York")

def get_eastern_now():
    """Get current time in Eastern Time"""
    return datetime.now(EASTERN)

[1161 more lines in file. Use offset=31 to continue.]


async def dashboard(request: Request):
    """Main dashboard showing all projects"""
    # Check authentication
    if not check_auth(request):
        return RedirectResponse(url="/login", status_code=303)
    # ALWAYS load from cache first - never block on SMB scan
    projects = await database.get_all_projects()

    # If cache is empty, trigger background scan (don't wait for it)
    needs_scan = not projects
    if needs_scan:
        asyncio.create_task(background_scan())

    # Separate by status
    marketing_projects = [p for p in projects if p.status == ProjectStatus.MARKETING]
    active_projects = [p for p in projects if p.status == ProjectStatus.ACTIVE]

    # Stats
    total_compliant = len([p for p in projects if p.score >= 90])
    total_at_risk = len([p for p in projects if p.score < 70])

    # Get most active projects for quick access sections
    loop = asyncio.get_event_loop()
    most_active_projects, most_active_marketing = await asyncio.gather(
        loop.run_in_executor(None, scanner.get_most_active_by_status, ProjectStatus.ACTIVE, 5),
        loop.run_in_executor(None, scanner.get_most_active_by_status, ProjectStatus.MARKETING, 5)
    )

    return templates.TemplateResponse("dashboard.html", {
        "request": request,
        "marketing_projects": marketing_projects,
        "active_projects": active_projects,
        "total_projects": len(projects),
        "total_compliant": total_compliant,
        "total_at_risk": total_at_risk,
        "last_scan": get_eastern_now().strftime("%Y-%m-%d %I:%M %p ET"),
        "scan_in_progress": needs_scan or _scan_in_progress,
        "most_active_projects": most_active_projects,
        "most_active_marketing": most_active_marketing
    })

@app.get("/project/{project_name}", response_class=HTMLResponse)
async def project_detail(request: Request, project_name: str, refresh: bool = False):
    """Project detail view with full compliance breakdown"""

[869 more lines in file. Use offset=323 to continue.]


Now I'll add the permit lookup to the dashboard function and update the template:

edit

async def dashboard(request: Request):
    """Main dashboard showing all projects"""
    # Check authentication
    if not check_auth(request):
        return RedirectResponse(url="/login", status_code=303)
    # ALWAYS load from cache first - never block on SMB scan
    projects = await database.get_all_projects()

    # If cache is empty, trigger background scan (don't wait for it)
    needs_scan = not projects
    if needs_scan:
        asyncio.create_task(background_scan())

    # Separate by status
    marketing_projects = [p for p in projects if p.status == ProjectStatus.MARKETING]
    active_projects = [p for p in projects if p.status == ProjectStatus.ACTIVE]

    # Stats
    total_compliant = len([p for p in projects if p.score >= 90])
    total_at_risk = len([p for p in projects if p.score < 70])

    # Get most active projects for quick access sections
    loop = asyncio.get_event_loop()
    most_active_projects, most_active_marketing = await asyncio.gather(
        loop.run_in_executor(None, scanner.get_most_active_by_status, ProjectStatus.ACTIVE, 5),
        loop.run_in_executor(None, scanner.get_most_active_by_status, ProjectStatus.MARKETING, 5)
    )

    return templates.TemplateResponse("dashboard.html", {
        "request": request,
        "marketing_projects": marketing_projects,
        "active_projects": active_projects,
        "total_projects": len(projects),
        "total_compliant": total_compliant,
        "total_at_risk": total_at_risk,
        "last_scan": get_eastern_now().strftime("%Y-%m-%d %I:%M %p ET"),
        "scan_in_progress": needs_scan or _scan_in_progress,
        "most_active_projects": most_active_projects,
        "most_active_marketing": most_active_marketing
    })

async def dashboard(request: Request):
    """Main dashboard showing all projects"""
    # Check authentication
    if not check_auth(request):
        return RedirectResponse(url="/login", status_code=303)
    # ALWAYS load from cache first - never block on SMB scan
    projects = await database.get_all_projects()

    # If cache is empty, trigger background scan (don't wait for it)
    needs_scan = not projects
    if needs_scan:
        asyncio.create_task(background_scan())

    # Separate by status
    marketing_projects = [p for p in projects if p.status == ProjectStatus.MARKETING]
    active_projects = [p for p in projects if p.status == ProjectStatus.ACTIVE]

    # Stats
    total_compliant = len([p for p in projects if p.score >= 90])
    total_at_risk = len([p for p in projects if p.score < 70])

    # Get most active projects for quick access sections
    loop = asyncio.get_event_loop()
    most_active_projects, most_active_marketing = await asyncio.gather(
        loop.run_in_executor(None, scanner.get_most_active_by_status, ProjectStatus.ACTIVE, 5),
        loop.run_in_executor(None, scanner.get_most_active_by_status, ProjectStatus.MARKETING, 5)
    )

    # Build permit map: {custom_id -> permit row} for cards
    permit_map = await database.get_permit_map()

    return templates.TemplateResponse("dashboard.html", {
        "request": request,
        "marketing_projects": marketing_projects,
        "active_projects": active_projects,
        "total_projects": len(projects),
        "total_compliant": total_compliant,
        "total_at_risk": total_at_risk,
        "last_scan": get_eastern_now().strftime("%Y-%m-%d %I:%M %p ET"),
        "scan_in_progress": needs_scan or _scan_in_progress,
        "most_active_projects": most_active_projects,
        "most_active_marketing": most_active_marketing,
        "permit_map": permit_map,
    })


Successfully replaced text in /home/crogers2287/wm/main.py.

      ...
  302         loop.run_in_executor(None, scanner.get_most_active_by_status, ProjectStatus.ACTIVE, 5),
  303         loop.run_in_executor(None, scanner.get_most_active_by_status, ProjectStatus.MARKETING, 5)
  304     )
  305
+ 306     # Build permit map: {custom_id -> permit row} for cards
+ 307     permit_map = await database.get_permit_map()
+ 308
  306     return templates.TemplateResponse("dashboard.html", {
  307         "request": request,
  308         "marketing_projects": marketing_projects,
  309         "active_projects": active_projects,
  310         "total_projects": len(projects),
  311         "total_compliant": total_compliant,
  312         "total_at_risk": total_at_risk,
  313         "last_scan": get_eastern_now().strftime("%Y-%m-%d %I:%M %p ET"),
  314         "scan_in_progress": needs_scan or _scan_in_progress,
  315         "most_active_projects": most_active_projects,
- 316         "most_active_marketing": most_active_marketing
+ 319         "most_active_marketing": most_active_marketing,
+ 320         "permit_map": permit_map,
  317     })
  318
  319
  320 @app.get("/project/{project_name}", response_class=HTMLResponse)
      ...


Now add `get_permit_map` to database.py:


"""ASH Compliance Tracker - Database Layer"""
import aiosqlite
from datetime import datetime
from pathlib import Path
import json
from typing import Optional
from models import ProjectCompliance, TradeCompliance, ProjectStatus, ComplianceStatus, ChecklistData

DB_PATH = Path(__file__).parent / "compliance.db"

async def init_db():
    """Initialize the database with WAL mode for better concurrency"""
    async with aiosqlite.connect(DB_PATH, timeout=3.0) as db:
        # Enable WAL mode for better concurrent read/write performance
        await db.execute("PRAGMA journal_mode=WAL")
        await db.execute("PRAGMA busy_timeout=3000")  # 30 second timeout
        await db.execute("""
            CREATE TABLE IF NOT EXISTS projects (
                name TEXT PRIMARY KEY,

[325 more lines in file. Use offset=21 to continue.]


    """Load all permits from DB — sorted active first, then by agency, then by date"""
    async with aiosqlite.connect(DB_PATH, timeout=3.0) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute("""
            SELECT * FROM permits
            ORDER BY
                CASE status_class WHEN 'blue' THEN 0 WHEN 'yellow' THEN 1 ELSE 2 END,
                COALESCE(agency, ''),
                updated_date DESC
        """) as cursor:
            rows = await cursor.fetchall()
    result = []
    for r in rows:
        d = dict(r)
        d["documents"] = json.loads(d.get("documents_json") or "[]")
        result.append(d)
    return result

async def get_permit_sync_status() -> dict:

[11 more lines in file. Use offset=335 to continue.]


    """Get last sync time and count"""
    async with aiosqlite.connect(DB_PATH, timeout=3.0) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            "SELECT synced_at, permit_count FROM permit_sync_log ORDER BY id DESC LIMIT 1"
        ) as cursor:
            row = await cursor.fetchone()
    if row:
        return {"last_sync": row["synced_at"], "count": row["permit_count"]}
    return {"last_sync": None, "count": 0}


async def get_permit_map() -> dict:
    """Return {custom_id: permit_dict} for all permits — used to annotate dashboard cards."""
    async with aiosqlite.connect(DB_PATH, timeout=3.0) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            "SELECT custom_id, status, status_class, address, description, record_type, municipality_label, href FROM permits"
        ) as cursor:
            rows = await cursor.fetchall()
    result = {}
    for r in rows:
        d = dict(r)
        result[d["custom_id"]] = d
    return result

async def get_permit_sync_status() -> dict:
    """Get last sync time and count"""


Successfully replaced text in /home/crogers2287/wm/database.py.

     ...
 330         result.append(d)
 331     return result
 332
 333
+334 async def get_permit_map() -> dict:
+335     """Return {custom_id: permit_dict} for all permits — used to annotate dashboard cards."""
+336     async with aiosqlite.connect(DB_PATH, timeout=3.0) as db:
+337         db.row_factory = aiosqlite.Row
+338         async with db.execute(
+339             "SELECT custom_id, status, status_class, address, description, record_type, municipality_label, href FROM permits"
+340         ) as cursor:
+341             rows = await cursor.fetchall()
+342     result = {}
+343     for r in rows:
+344         d = dict(r)
+345         result[d["custom_id"]] = d
+346     return result
+347
+348
 334 async def get_permit_sync_status() -> dict:
 335     """Get last sync time and count"""
 336     async with aiosqlite.connect(DB_PATH, timeout=3.0) as db:
 337         db.row_factory = aiosqlite.Row
     ...


Now update the dashboard template to show permit info on the cards. I'll add a permit status pill and description snippet between the header and the MEP section:

    .card-actions {
        opacity: 0;
        transition: opacity 0.2s;
    }
    .project-card:hover .card-actions {
        opacity: 1;
    }
    @media (max-width: 768px) {
        .card-actions {
            opacity: 1;
        }
    }
    .permit-pill {
        display: inline-flex;
        align-items: center;
        gap: 4px;
        padding: 2px 7px;
        border-radius: 99px;
        font-size: 10px;
        font-weight: 600;
        letter-spacing: 0.02em;
        white-space: nowrap;
    }
    .permit-pill.blue  { background: #dbeafe; color: #1d4ed8; }
    .permit-pill.green { background: #dcfce7; color: #15803d; }
    .permit-pill.yellow{ background: #fef9c3; color: #a16207; }
    .permit-pill.red   { background: #fee2e2; color: #b91c1c; }
    .permit-pill.gray  { background: #f1f5f9; color: #64748b; }
    .permit-bar {
        padding: 7px 14px;
        border-top: 1px solid #f1f5f9;
        display: flex;
        align-items: flex-start;
        gap: 7px;
    }
    .permit-desc {
        font-size: 11px;
        color: #64748b;
        line-height: 1.4;
        min-width: 0;
        flex: 1;
        overflow: hidden;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
    }


Successfully replaced text in /home/crogers2287/wm/templates/dashboard.html.

     ...
  27         .card-actions {
  28             opacity: 1;
  29         }
  30     }
+ 31     .permit-pill {
+ 32         display: inline-flex;
+ 33         align-items: center;
+ 34         gap: 4px;
+ 35         padding: 2px 7px;
+ 36         border-radius: 99px;
+ 37         font-size: 10px;
+ 38         font-weight: 600;
+ 39         letter-spacing: 0.02em;
+ 40         white-space: nowrap;
+ 41     }
+ 42     .permit-pill.blue  { background: #dbeafe; color: #1d4ed8; }
+ 43     .permit-pill.green { background: #dcfce7; color: #15803d; }
+ 44     .permit-pill.yellow{ background: #fef9c3; color: #a16207; }
+ 45     .permit-pill.red   { background: #fee2e2; color: #b91c1c; }
+ 46     .permit-pill.gray  { background: #f1f5f9; color: #64748b; }
+ 47     .permit-bar {
+ 48         padding: 7px 14px;
+ 49         border-top: 1px solid #f1f5f9;
+ 50         display: flex;
+ 51         align-items: flex-start;
+ 52         gap: 7px;
+ 53     }
+ 54     .permit-desc {
+ 55         font-size: 11px;
+ 56         color: #64748b;
+ 57         line-height: 1.4;
+ 58         min-width: 0;
+ 59         flex: 1;
+ 60         overflow: hidden;
+ 61         display: -webkit-box;
+ 62         -webkit-line-clamp: 2;
+ 63         -webkit-box-orient: vertical;
+ 64     }
  31 </style>
  32
  33 <!-- Header with logout -->
  34 <div class="flex justify-between items-start mb-6">
     ...


Now add the permit bar to both active and marketing project cards. I'll add it right after the card header (before the MEP section). Since the structure is the same for both, I'll use a macro-style approach — add it in both places:

{% for project in active_projects|sort(attribute='total_proposals', reverse=true) %}

                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
                    </svg>

                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"/>
                    </svg>

                                {{ project.name }}

                                {% if project.checklist and project.checklist.municipality %}
                                    {{ project.checklist.municipality }}
                                {% endif %}
                                {% if project.checklist and project.checklist.permit_number %}
                                    {% if project.checklist.municipality %}|{% endif %}
                                    #{{ project.checklist.permit_number }}
                                {% endif %}

{% for project in active_projects|sort(attribute='total_proposals', reverse=true) %}
        {% set permit_num = project.checklist.permit_number if project.checklist else '' %}
        {% set permit = permit_map.get(permit_num) if permit_num and permit_num != 'TBD' else none %}

                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
                    </svg>

                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"/>
                    </svg>

                                {{ project.name }}

                                {% if project.checklist and project.checklist.municipality %}
                                    {{ project.checklist.municipality }}
                                {% endif %}
                                {% if permit_num and permit_num != 'TBD' %}
                                    {% if project.checklist.municipality %}|{% endif %}
                                    #{{ permit_num }}
                                {% endif %}

                {% if permit %}

                        {{ permit.status }}

                    {% set desc = permit.description or permit.address or '' %}
                    {% if desc %}
                    {{ desc }}
                    {% endif %}

                {% elif permit_num and permit_num != 'TBD' %}

                    No sync data
                    {{ permit_num }}

                {% endif %}


...
 114

 115
 116

 117         {% for project in active_projects|sort(attribute='total_proposals', reverse=true) %}
+118         {% set permit_num = project.checklist.permit_number if project.checklist else '' %}
+119         {% set permit = permit_map.get(permit_num) if permit_num and permit_num != 'TBD' else none %}
 118

 119
 120

 121
 125                     <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
 126                         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
 127                     </svg>
 128
 129
 133                     <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
 134                         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"/>
 135                     </svg>
 136
 137

 138
 139
 140
 141

 142

 143

 144

 145                                 {{ project.name }}
 146

 147

 148                                 {% if project.checklist and project.checklist.municipality %}
 149                                     {{ project.checklist.municipality }}
 150                                 {% endif %}
-151                                 {% if project.checklist and project.checklist.permit_number %}
+153                                 {% if permit_num and permit_num != 'TBD' %}
 152                                     {% if project.checklist.municipality %}|{% endif %}
-153                                     #{{ project.checklist.permit_number }}
+155                                     #{{ permit_num }}
 154                                 {% endif %}
 155

 156

 157

 158

 159
+162
+163                 {% if permit %}
+164

+165
+166                         {{ permit.status }}
+167
+168                     {% set desc = permit.description or permit.address or '' %}
+169                     {% if desc %}
+170                     {{ desc }}
+171                     {% endif %}
+172

+173                 {% elif permit_num and permit_num != 'TBD' %}
+174

+175                     No sync data
+176                     {{ permit_num }}
+177

+178                 {% endif %}
+179
 160
 161

 162                     {% set plumbing_sub = project.get_mep_sub('Plumbing') %}
 163                     {% set hvac_sub = project.get_mep_sub('HVAC') %}
     ...


Now do the same for the marketing/bidding cards:

{% for project in marketing_projects|sort(attribute='total_proposals', reverse=true) %}

                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
                    </svg>

                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"/>
                    </svg>

                                {{ project.name }}

                                {% if project.checklist and project.checklist.municipality %}
                                    {{ project.checklist.municipality }}
                                {% endif %}
                                {% if project.checklist and project.checklist.permit_number %}
                                    {% if project.checklist.municipality %}|{% endif %}
                                    #{{ project.checklist.permit_number }}
                                {% endif %}

{% for project in marketing_projects|sort(attribute='total_proposals', reverse=true) %}
        {% set permit_num = project.checklist.permit_number if project.checklist else '' %}
        {% set permit = permit_map.get(permit_num) if permit_num and permit_num != 'TBD' else none %}

                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
                    </svg>

                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"/>
                    </svg>

                                {{ project.name }}

                                {% if project.checklist and project.checklist.municipality %}
                                    {{ project.checklist.municipality }}
                                {% endif %}
                                {% if permit_num and permit_num != 'TBD' %}
                                    {% if project.checklist.municipality %}|{% endif %}
                                    #{{ permit_num }}
                                {% endif %}

                {% if permit %}

                        {{ permit.status }}

                    {% set desc = permit.description or permit.address or '' %}
                    {% if desc %}
                    {{ desc }}
                    {% endif %}

                {% elif permit_num and permit_num != 'TBD' %}

                    No sync data
                    {{ permit_num }}

                {% endif %}